@jaak.ai/stamps 2.0.0 → 2.1.0-dev.10
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.
- package/README.md +11 -1
- package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
- package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
- package/dist/cjs/{index-BfhtOB0D.js → index-Ga0t6BMe.js} +176 -119
- package/dist/cjs/index-Ga0t6BMe.js.map +1 -0
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js +5 -4
- package/dist/cjs/jaak-stamps-webcomponent.cjs.js.map +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +1174 -655
- package/dist/cjs/jaak-stamps.cjs.entry.js.map +1 -1
- package/dist/cjs/jaak-stamps.entry.cjs.js.map +1 -1
- package/dist/cjs/loader.cjs.js +4 -3
- package/dist/cjs/loader.cjs.js.map +1 -1
- package/dist/collection/collection-manifest.json +1 -1
- package/dist/collection/components/my-component/my-component.css +224 -13
- package/dist/collection/components/my-component/my-component.js +950 -281
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +156 -344
- package/dist/collection/services/CameraService.js.map +1 -1
- package/dist/collection/services/DetectionService.js +100 -42
- package/dist/collection/services/DetectionService.js.map +1 -1
- package/dist/collection/services/EventBusService.js +1 -1
- package/dist/collection/services/EventBusService.js.map +1 -1
- package/dist/collection/services/ServiceContainer.js +4 -13
- package/dist/collection/services/ServiceContainer.js.map +1 -1
- package/dist/collection/services/interfaces/ICameraService.js.map +1 -1
- package/dist/collection/types/component-types.js.map +1 -1
- package/dist/components/index.js +165 -113
- package/dist/components/index.js.map +1 -1
- package/dist/components/jaak-stamps.js +1182 -658
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/app-globals-DQuL1Twl.js +6 -0
- package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
- package/dist/esm/{index-BP1Q4KOg.js → index-Drbzcuq-.js} +177 -119
- package/dist/esm/index-Drbzcuq-.js.map +1 -0
- package/dist/esm/jaak-stamps-webcomponent.js +5 -4
- package/dist/esm/jaak-stamps-webcomponent.js.map +1 -1
- package/dist/esm/jaak-stamps.entry.js +1174 -655
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +4 -3
- package/dist/esm/loader.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/loader.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-5f7301f1.entry.js.map +1 -0
- package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-DQuL1Twl.js.map +1 -0
- package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js +3 -0
- package/dist/jaak-stamps-webcomponent/p-Drbzcuq-.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +87 -17
- package/dist/types/components.d.ts +14 -8
- package/dist/types/services/CameraService.d.ts +12 -14
- package/dist/types/services/DetectionService.d.ts +10 -3
- package/dist/types/services/ServiceContainer.d.ts +1 -2
- package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
- package/dist/types/stencil-public-runtime.d.ts +12 -5
- package/dist/types/types/component-types.d.ts +3 -0
- package/package.json +4 -2
- package/dist/cjs/index-BfhtOB0D.js.map +0 -1
- package/dist/collection/services/LoggerService.js +0 -40
- package/dist/collection/services/LoggerService.js.map +0 -1
- package/dist/collection/services/interfaces/ILogger.js +0 -2
- package/dist/collection/services/interfaces/ILogger.js.map +0 -1
- package/dist/esm/index-BP1Q4KOg.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
- package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js +0 -3
- package/dist/jaak-stamps-webcomponent/p-BP1Q4KOg.js.map +0 -1
- package/dist/types/services/LoggerService.d.ts +0 -12
- package/dist/types/services/interfaces/ILogger.d.ts +0 -8
|
@@ -1,44 +1,4 @@
|
|
|
1
|
-
import { r as registerInstance, c as createEvent,
|
|
2
|
-
|
|
3
|
-
class LoggerService {
|
|
4
|
-
debugMode;
|
|
5
|
-
constructor(debug = false) {
|
|
6
|
-
this.debugMode = debug;
|
|
7
|
-
}
|
|
8
|
-
setDebugMode(debug) {
|
|
9
|
-
this.debugMode = debug;
|
|
10
|
-
}
|
|
11
|
-
info(...args) {
|
|
12
|
-
if (this.debugMode) {
|
|
13
|
-
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
warn(...args) {
|
|
17
|
-
if (this.debugMode) {
|
|
18
|
-
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
error(...args) {
|
|
22
|
-
if (this.debugMode) {
|
|
23
|
-
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
debug(...args) {
|
|
27
|
-
if (this.debugMode) {
|
|
28
|
-
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
state(state, data) {
|
|
32
|
-
if (this.debugMode) {
|
|
33
|
-
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
performance(operation, duration) {
|
|
37
|
-
if (this.debugMode) {
|
|
38
|
-
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import { r as registerInstance, c as createEvent, g as getElement, h } from './index-Drbzcuq-.js';
|
|
42
2
|
|
|
43
3
|
class EventBusService {
|
|
44
4
|
events = new Map();
|
|
@@ -65,7 +25,7 @@ class EventBusService {
|
|
|
65
25
|
callback(data);
|
|
66
26
|
}
|
|
67
27
|
catch (error) {
|
|
68
|
-
|
|
28
|
+
// Silently handle callback errors
|
|
69
29
|
}
|
|
70
30
|
});
|
|
71
31
|
}
|
|
@@ -192,16 +152,20 @@ class StateManagerService {
|
|
|
192
152
|
}
|
|
193
153
|
|
|
194
154
|
class CameraService {
|
|
195
|
-
logger;
|
|
196
155
|
eventBus;
|
|
197
156
|
availableCameras = [];
|
|
198
157
|
selectedCameraId = null;
|
|
199
158
|
deviceType = 'desktop';
|
|
200
159
|
preferredCameraFacing = null;
|
|
201
160
|
preferredCamera = 'auto';
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
161
|
+
// Optimization: Static caches for performance
|
|
162
|
+
static cameraCapabilitiesCache = new Map();
|
|
163
|
+
static deviceEnumerationCache = null;
|
|
164
|
+
static CACHE_DURATION = 30000; // 30 seconds
|
|
165
|
+
// Stream management for optimization
|
|
166
|
+
currentStreamPromise;
|
|
167
|
+
lastStreamConstraints;
|
|
168
|
+
constructor(eventBus, preferredCamera = 'auto') {
|
|
205
169
|
this.eventBus = eventBus;
|
|
206
170
|
this.preferredCamera = preferredCamera;
|
|
207
171
|
}
|
|
@@ -218,18 +182,21 @@ class CameraService {
|
|
|
218
182
|
else {
|
|
219
183
|
this.deviceType = 'desktop';
|
|
220
184
|
}
|
|
221
|
-
this.logger.state('DISPOSITIVO_DETECTADO', {
|
|
222
|
-
deviceType: this.deviceType,
|
|
223
|
-
userAgent: navigator.userAgent,
|
|
224
|
-
screenDimensions: { width: window.innerWidth, height: window.innerHeight }
|
|
225
|
-
});
|
|
226
185
|
return this.deviceType;
|
|
227
186
|
}
|
|
228
187
|
async enumerateDevices() {
|
|
229
188
|
try {
|
|
189
|
+
// Check cache first for optimization
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (CameraService.deviceEnumerationCache &&
|
|
192
|
+
(now - CameraService.deviceEnumerationCache.timestamp) < CameraService.CACHE_DURATION) {
|
|
193
|
+
this.availableCameras = CameraService.deviceEnumerationCache.devices;
|
|
194
|
+
await this.setInitialCameraPreference();
|
|
195
|
+
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
196
|
+
return this.availableCameras;
|
|
197
|
+
}
|
|
230
198
|
const permissionStatus = await this.checkCameraPermission();
|
|
231
199
|
if (permissionStatus === 'denied') {
|
|
232
|
-
this.logger.error('Permiso de cámara denegado por el usuario');
|
|
233
200
|
return [];
|
|
234
201
|
}
|
|
235
202
|
if (permissionStatus === 'prompt') {
|
|
@@ -238,24 +205,16 @@ class CameraService {
|
|
|
238
205
|
}
|
|
239
206
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
240
207
|
this.availableCameras = devices.filter(device => device.kind === 'videoinput');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
this.setInitialCameraPreference();
|
|
249
|
-
this.logger.state('PREFERENCIA_INICIAL_APLICADA', {
|
|
250
|
-
selectedCameraId: this.selectedCameraId,
|
|
251
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
252
|
-
preferredCamera: this.preferredCamera
|
|
253
|
-
});
|
|
208
|
+
// Cache the results for optimization
|
|
209
|
+
CameraService.deviceEnumerationCache = {
|
|
210
|
+
devices: this.availableCameras,
|
|
211
|
+
timestamp: now
|
|
212
|
+
};
|
|
213
|
+
await this.setInitialCameraPreference();
|
|
254
214
|
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
255
215
|
return this.availableCameras;
|
|
256
216
|
}
|
|
257
217
|
catch (error) {
|
|
258
|
-
this.logger.error('Error al enumerar cámaras disponibles:', error);
|
|
259
218
|
this.handleCameraPermissionError(error);
|
|
260
219
|
return [];
|
|
261
220
|
}
|
|
@@ -276,8 +235,6 @@ class CameraService {
|
|
|
276
235
|
}
|
|
277
236
|
this.selectedCameraId = cameraId;
|
|
278
237
|
this.updatePreferredFacing(camera);
|
|
279
|
-
this.isManuallySelected = true; // Mark as manually selected
|
|
280
|
-
this.savePreference();
|
|
281
238
|
this.eventBus.emit('camera-changed', cameraId);
|
|
282
239
|
}
|
|
283
240
|
getPreferredFacing() {
|
|
@@ -286,14 +243,20 @@ class CameraService {
|
|
|
286
243
|
async setupCamera(constraints) {
|
|
287
244
|
try {
|
|
288
245
|
const finalConstraints = constraints || await this.getMaxResolution();
|
|
289
|
-
|
|
246
|
+
// Optimization: Check if we can reuse existing stream with same constraints
|
|
247
|
+
if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
|
|
248
|
+
return await this.currentStreamPromise;
|
|
249
|
+
}
|
|
250
|
+
// Create new stream promise for lazy loading
|
|
251
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
290
252
|
video: finalConstraints,
|
|
291
253
|
audio: false
|
|
292
254
|
});
|
|
255
|
+
this.lastStreamConstraints = finalConstraints;
|
|
256
|
+
const stream = await this.currentStreamPromise;
|
|
293
257
|
return stream;
|
|
294
258
|
}
|
|
295
259
|
catch (error) {
|
|
296
|
-
this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
|
|
297
260
|
// Fallback to basic constraints if getMaxResolution fails
|
|
298
261
|
const basicConstraints = {
|
|
299
262
|
width: { ideal: 1280 },
|
|
@@ -303,65 +266,21 @@ class CameraService {
|
|
|
303
266
|
if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
|
|
304
267
|
basicConstraints.facingMode = this.preferredCameraFacing;
|
|
305
268
|
}
|
|
306
|
-
this.
|
|
307
|
-
return await navigator.mediaDevices.getUserMedia({
|
|
269
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
308
270
|
video: basicConstraints,
|
|
309
271
|
audio: false
|
|
310
272
|
});
|
|
273
|
+
this.lastStreamConstraints = basicConstraints;
|
|
274
|
+
return await this.currentStreamPromise;
|
|
311
275
|
}
|
|
312
276
|
}
|
|
313
277
|
async switchCamera(cameraId) {
|
|
314
278
|
const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
315
279
|
if (!selectedCamera) {
|
|
316
|
-
this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
|
|
317
280
|
await this.enumerateDevices();
|
|
318
281
|
return;
|
|
319
282
|
}
|
|
320
283
|
await this.setSelectedCamera(cameraId);
|
|
321
|
-
this.logger.state('CAMARA_CAMBIADA', {
|
|
322
|
-
label: selectedCamera.label,
|
|
323
|
-
deviceId: selectedCamera.deviceId,
|
|
324
|
-
isManuallySelected: this.isManuallySelected
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
async flipToNextCamera() {
|
|
328
|
-
if (!this.isMultipleCamerasAvailable())
|
|
329
|
-
return;
|
|
330
|
-
const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
|
|
331
|
-
const nextIndex = (currentIndex + 1) % this.availableCameras.length;
|
|
332
|
-
const nextCamera = this.availableCameras[nextIndex];
|
|
333
|
-
await this.switchCamera(nextCamera.deviceId);
|
|
334
|
-
}
|
|
335
|
-
savePreference() {
|
|
336
|
-
try {
|
|
337
|
-
const preference = {
|
|
338
|
-
cameraId: this.selectedCameraId,
|
|
339
|
-
facing: this.preferredCameraFacing,
|
|
340
|
-
timestamp: Date.now()
|
|
341
|
-
};
|
|
342
|
-
localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
|
|
343
|
-
this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
|
|
344
|
-
}
|
|
345
|
-
catch (error) {
|
|
346
|
-
this.logger.warn('Error al guardar preferencia de cámara:', error);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
loadPreference() {
|
|
350
|
-
try {
|
|
351
|
-
const saved = localStorage.getItem('jaak-stamps-camera-preference');
|
|
352
|
-
if (saved) {
|
|
353
|
-
const preference = JSON.parse(saved);
|
|
354
|
-
const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
|
|
355
|
-
if (isStillAvailable) {
|
|
356
|
-
this.selectedCameraId = preference.cameraId;
|
|
357
|
-
this.preferredCameraFacing = preference.facing;
|
|
358
|
-
this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
this.logger.warn('Error al cargar preferencia de cámara:', error);
|
|
364
|
-
}
|
|
365
284
|
}
|
|
366
285
|
isRearCamera(stream) {
|
|
367
286
|
const videoTrack = stream.getVideoTracks()[0];
|
|
@@ -370,13 +289,18 @@ class CameraService {
|
|
|
370
289
|
const settings = videoTrack.getSettings();
|
|
371
290
|
return settings.facingMode === 'environment';
|
|
372
291
|
}
|
|
373
|
-
getCameraInfo() {
|
|
374
|
-
|
|
375
|
-
|
|
292
|
+
async getCameraInfo() {
|
|
293
|
+
const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
|
|
294
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
295
|
+
return {
|
|
376
296
|
id: camera.deviceId,
|
|
377
297
|
label: camera.label || 'Unknown Camera',
|
|
378
|
-
selected: camera.deviceId === this.selectedCameraId
|
|
379
|
-
|
|
298
|
+
selected: camera.deviceId === this.selectedCameraId,
|
|
299
|
+
hasAutofocus
|
|
300
|
+
};
|
|
301
|
+
}));
|
|
302
|
+
return {
|
|
303
|
+
availableCameras: availableCamerasWithAutofocus,
|
|
380
304
|
selectedCameraId: this.selectedCameraId,
|
|
381
305
|
deviceType: this.deviceType,
|
|
382
306
|
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
|
|
@@ -392,7 +316,6 @@ class CameraService {
|
|
|
392
316
|
return permission.state;
|
|
393
317
|
}
|
|
394
318
|
catch (error) {
|
|
395
|
-
this.logger.warn('No se pudo verificar permisos de cámara:', error);
|
|
396
319
|
return 'prompt';
|
|
397
320
|
}
|
|
398
321
|
}
|
|
@@ -400,35 +323,48 @@ class CameraService {
|
|
|
400
323
|
this.availableCameras = [];
|
|
401
324
|
this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
|
|
402
325
|
}
|
|
403
|
-
setInitialCameraPreference() {
|
|
326
|
+
async setInitialCameraPreference() {
|
|
404
327
|
if (this.availableCameras.length === 0)
|
|
405
328
|
return;
|
|
406
|
-
// Clear any existing localStorage preferences to ensure fresh start
|
|
407
|
-
localStorage.removeItem('jaak-stamps-camera-preference');
|
|
408
|
-
this.logger.state('APLICANDO_PREFERENCIA_INICIAL', {
|
|
409
|
-
preferredCamera: this.preferredCamera,
|
|
410
|
-
availableCamerasCount: this.availableCameras.length
|
|
411
|
-
});
|
|
412
|
-
this.isManuallySelected = false; // Reset manual selection flag
|
|
413
329
|
if (this.preferredCamera === 'front') {
|
|
414
|
-
this.selectFrontCamera();
|
|
330
|
+
await this.selectFrontCamera();
|
|
415
331
|
}
|
|
416
332
|
else if (this.preferredCamera === 'back') {
|
|
417
|
-
this.selectBackCamera();
|
|
333
|
+
await this.selectBackCamera();
|
|
418
334
|
}
|
|
419
335
|
else {
|
|
420
|
-
this.selectAutoCamera();
|
|
336
|
+
await this.selectAutoCamera();
|
|
421
337
|
}
|
|
422
338
|
}
|
|
423
|
-
|
|
339
|
+
async checkCameraAutofocus(deviceId) {
|
|
340
|
+
try {
|
|
341
|
+
// Check cache first for optimization
|
|
342
|
+
if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
|
|
343
|
+
const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
|
|
344
|
+
return typeof cached === 'boolean' ? cached : false;
|
|
345
|
+
}
|
|
346
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
347
|
+
video: { deviceId: { exact: deviceId } }
|
|
348
|
+
});
|
|
349
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
350
|
+
const capabilities = videoTrack.getCapabilities();
|
|
351
|
+
stream.getTracks().forEach(track => track.stop());
|
|
352
|
+
const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
|
|
353
|
+
const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
|
|
354
|
+
capabilities.focusMode.includes('auto'));
|
|
355
|
+
// Cache the result for future use
|
|
356
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
|
|
357
|
+
return hasAutofocus;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
// Cache negative result to avoid repeated failures
|
|
361
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, false);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async selectFrontCamera() {
|
|
424
366
|
this.preferredCameraFacing = 'user';
|
|
425
|
-
this.
|
|
426
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
427
|
-
id: cam.deviceId,
|
|
428
|
-
label: cam.label
|
|
429
|
-
}))
|
|
430
|
-
});
|
|
431
|
-
const frontCamera = this.availableCameras.find(camera => {
|
|
367
|
+
const frontCameras = this.availableCameras.filter(camera => {
|
|
432
368
|
const label = camera.label.toLowerCase();
|
|
433
369
|
return label.includes('front') ||
|
|
434
370
|
label.includes('user') ||
|
|
@@ -436,24 +372,24 @@ class CameraService {
|
|
|
436
372
|
label.includes('frontal') ||
|
|
437
373
|
(!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
|
|
438
374
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
375
|
+
let selectedCamera = null;
|
|
376
|
+
if (frontCameras.length > 0) {
|
|
377
|
+
for (const camera of frontCameras) {
|
|
378
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
379
|
+
if (hasAutofocus) {
|
|
380
|
+
selectedCamera = camera;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!selectedCamera) {
|
|
385
|
+
selectedCamera = frontCameras[0];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
446
389
|
}
|
|
447
|
-
selectBackCamera() {
|
|
390
|
+
async selectBackCamera() {
|
|
448
391
|
this.preferredCameraFacing = 'environment';
|
|
449
|
-
this.
|
|
450
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
451
|
-
id: cam.deviceId,
|
|
452
|
-
label: cam.label
|
|
453
|
-
}))
|
|
454
|
-
});
|
|
455
|
-
// Try to find rear camera with multiple detection methods
|
|
456
|
-
let backCamera = this.availableCameras.find(camera => {
|
|
392
|
+
const backCameras = this.availableCameras.filter(camera => {
|
|
457
393
|
const label = camera.label.toLowerCase();
|
|
458
394
|
return label.includes('back') ||
|
|
459
395
|
label.includes('rear') ||
|
|
@@ -461,34 +397,33 @@ class CameraService {
|
|
|
461
397
|
label.includes('trasera') ||
|
|
462
398
|
label.includes('posterior');
|
|
463
399
|
});
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
400
|
+
let selectedCamera = null;
|
|
401
|
+
if (backCameras.length > 0) {
|
|
402
|
+
for (const camera of backCameras) {
|
|
403
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
404
|
+
if (hasAutofocus) {
|
|
405
|
+
selectedCamera = camera;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!selectedCamera) {
|
|
410
|
+
selectedCamera = backCameras[0];
|
|
411
|
+
}
|
|
471
412
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
413
|
+
else if (this.availableCameras.length > 1) {
|
|
414
|
+
const lastCamera = this.availableCameras[this.availableCameras.length - 1];
|
|
415
|
+
const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
|
|
416
|
+
selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
|
|
417
|
+
}
|
|
418
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
478
419
|
}
|
|
479
|
-
selectAutoCamera() {
|
|
420
|
+
async selectAutoCamera() {
|
|
480
421
|
if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
|
|
481
|
-
this.selectBackCamera();
|
|
482
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
|
|
422
|
+
await this.selectBackCamera();
|
|
483
423
|
}
|
|
484
424
|
else {
|
|
485
425
|
// For desktop, prefer front camera (usually built-in webcam)
|
|
486
|
-
this.selectFrontCamera();
|
|
487
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
|
|
488
|
-
type: 'front',
|
|
489
|
-
label: this.availableCameras[0].label,
|
|
490
|
-
deviceId: this.selectedCameraId
|
|
491
|
-
});
|
|
426
|
+
await this.selectFrontCamera();
|
|
492
427
|
}
|
|
493
428
|
}
|
|
494
429
|
updatePreferredFacing(camera) {
|
|
@@ -504,40 +439,19 @@ class CameraService {
|
|
|
504
439
|
await this.detectDeviceType();
|
|
505
440
|
}
|
|
506
441
|
const videoConstraints = {};
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
510
|
-
preferredCamera: this.preferredCamera,
|
|
511
|
-
deviceType: this.deviceType,
|
|
512
|
-
isManuallySelected: this.isManuallySelected
|
|
513
|
-
});
|
|
514
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
515
|
-
// When manually selected, use deviceId for exact camera selection
|
|
442
|
+
// Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
|
|
443
|
+
if (this.selectedCameraId) {
|
|
516
444
|
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
517
|
-
this.logger.state('USANDO_CAMARA_MANUAL', {
|
|
518
|
-
deviceId: this.selectedCameraId
|
|
519
|
-
});
|
|
520
445
|
}
|
|
446
|
+
// Priority 2: Use facingMode as fallback when no specific camera is selected
|
|
521
447
|
else if (this.preferredCameraFacing) {
|
|
522
|
-
// Use facingMode for initial preference-based selection
|
|
523
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
524
448
|
const facingConstraint = this.deviceType === 'desktop'
|
|
525
449
|
? { ideal: this.preferredCameraFacing }
|
|
526
450
|
: { exact: this.preferredCameraFacing };
|
|
527
451
|
videoConstraints.facingMode = facingConstraint;
|
|
528
|
-
this.logger.state('USANDO_FACING_CONSTRAINT', {
|
|
529
|
-
facingMode: this.preferredCameraFacing,
|
|
530
|
-
constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
else if (this.selectedCameraId) {
|
|
534
|
-
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
535
|
-
this.logger.state('USANDO_CAMARA_SELECCIONADA', {
|
|
536
|
-
deviceId: this.selectedCameraId
|
|
537
|
-
});
|
|
538
452
|
}
|
|
453
|
+
// Priority 3: Fallback to preferredCamera setting
|
|
539
454
|
else {
|
|
540
|
-
// Use preferredCamera setting to determine default facing mode
|
|
541
455
|
if (this.preferredCamera === 'front') {
|
|
542
456
|
videoConstraints.facingMode = "user";
|
|
543
457
|
}
|
|
@@ -545,24 +459,25 @@ class CameraService {
|
|
|
545
459
|
videoConstraints.facingMode = "environment";
|
|
546
460
|
}
|
|
547
461
|
else {
|
|
548
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
549
462
|
videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
550
463
|
}
|
|
551
|
-
this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
|
|
552
|
-
facingMode: videoConstraints.facingMode,
|
|
553
|
-
preferredCamera: this.preferredCamera,
|
|
554
|
-
deviceType: this.deviceType
|
|
555
|
-
});
|
|
556
464
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
465
|
+
// Optimization: Use cached capabilities if available
|
|
466
|
+
let capabilities;
|
|
467
|
+
if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
|
|
468
|
+
capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
|
|
472
|
+
const videoTrack = tempStream.getVideoTracks()[0];
|
|
473
|
+
capabilities = videoTrack.getCapabilities();
|
|
474
|
+
tempStream.getTracks().forEach(track => track.stop());
|
|
475
|
+
// Cache capabilities for future use
|
|
476
|
+
if (this.selectedCameraId) {
|
|
477
|
+
CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
563
480
|
const constraints = { ...videoConstraints };
|
|
564
|
-
// Enhanced focus and image quality settings
|
|
565
|
-
this.applyAdvancedCameraSettings(constraints, capabilities);
|
|
566
481
|
if (capabilities.width && capabilities.height) {
|
|
567
482
|
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
568
483
|
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
@@ -576,15 +491,20 @@ class CameraService {
|
|
|
576
491
|
constraints.height = { ideal: maxHeight };
|
|
577
492
|
}
|
|
578
493
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
494
|
+
// Configure autofocus if camera has the capability
|
|
495
|
+
const capabilitiesAny = capabilities;
|
|
496
|
+
if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
|
|
497
|
+
if (capabilitiesAny.focusMode.includes('continuous')) {
|
|
498
|
+
constraints.focusMode = 'continuous';
|
|
499
|
+
}
|
|
500
|
+
else if (capabilitiesAny.focusMode.includes('auto')) {
|
|
501
|
+
constraints.focusMode = 'auto';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
583
504
|
return constraints;
|
|
584
505
|
}
|
|
585
506
|
catch (err) {
|
|
586
|
-
|
|
587
|
-
// Ensure device type is detected before determining constraints
|
|
507
|
+
// Fallback logic with minimal temporary streams
|
|
588
508
|
if (!this.deviceType || this.deviceType === 'desktop') {
|
|
589
509
|
await this.detectDeviceType();
|
|
590
510
|
}
|
|
@@ -593,25 +513,16 @@ class CameraService {
|
|
|
593
513
|
width: { ideal: isTablet ? 1280 : 1920 },
|
|
594
514
|
height: { ideal: isTablet ? 720 : 1080 }
|
|
595
515
|
};
|
|
596
|
-
|
|
597
|
-
this.applyBasicFocusSettings(fallbackConstraints);
|
|
598
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
599
|
-
// When manually selected, use deviceId for exact camera selection
|
|
516
|
+
if (this.selectedCameraId) {
|
|
600
517
|
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
601
518
|
}
|
|
602
519
|
else if (this.preferredCameraFacing) {
|
|
603
|
-
// Use facingMode for initial preference-based selection
|
|
604
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
605
520
|
const facingConstraint = this.deviceType === 'desktop'
|
|
606
521
|
? { ideal: this.preferredCameraFacing }
|
|
607
522
|
: { exact: this.preferredCameraFacing };
|
|
608
523
|
fallbackConstraints.facingMode = facingConstraint;
|
|
609
524
|
}
|
|
610
|
-
else if (this.selectedCameraId) {
|
|
611
|
-
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
612
|
-
}
|
|
613
525
|
else {
|
|
614
|
-
// Use preferredCamera setting to determine default facing mode
|
|
615
526
|
if (this.preferredCamera === 'front') {
|
|
616
527
|
fallbackConstraints.facingMode = "user";
|
|
617
528
|
}
|
|
@@ -619,165 +530,26 @@ class CameraService {
|
|
|
619
530
|
fallbackConstraints.facingMode = "environment";
|
|
620
531
|
}
|
|
621
532
|
else {
|
|
622
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
623
533
|
fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
624
534
|
}
|
|
625
535
|
}
|
|
626
536
|
return fallbackConstraints;
|
|
627
537
|
}
|
|
628
538
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (capabilities.frameRate) {
|
|
633
|
-
constraints.frameRate = {
|
|
634
|
-
ideal: 30,
|
|
635
|
-
min: 15,
|
|
636
|
-
max: 60
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
// Width and height constraints for optimal resolution
|
|
640
|
-
if (capabilities.width && capabilities.height) {
|
|
641
|
-
constraints.width = {
|
|
642
|
-
ideal: Math.min(1920, capabilities.width.max || 1920),
|
|
643
|
-
min: 640
|
|
644
|
-
};
|
|
645
|
-
constraints.height = {
|
|
646
|
-
ideal: Math.min(1080, capabilities.height.max || 1080),
|
|
647
|
-
min: 480
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
// Apply enhanced constraints through advanced array for better browser compatibility
|
|
651
|
-
const advancedConstraints = [];
|
|
652
|
-
// Try to set optimal settings through advanced constraints
|
|
653
|
-
const advanced = {};
|
|
654
|
-
// Focus mode - use 'any' type to bypass TypeScript limitations
|
|
655
|
-
if (capabilities.focusMode) {
|
|
656
|
-
if (capabilities.focusMode.includes('continuous')) {
|
|
657
|
-
advanced.focusMode = 'continuous';
|
|
658
|
-
}
|
|
659
|
-
else if (capabilities.focusMode.includes('single-shot')) {
|
|
660
|
-
advanced.focusMode = 'single-shot';
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Focus distance for close objects
|
|
664
|
-
if (capabilities.focusDistance) {
|
|
665
|
-
advanced.focusDistance = {
|
|
666
|
-
ideal: 0.4, // 40cm - optimal for document capture
|
|
667
|
-
min: 0.2,
|
|
668
|
-
max: 1.0
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
// Zoom control
|
|
672
|
-
if (capabilities.zoom) {
|
|
673
|
-
advanced.zoom = {
|
|
674
|
-
ideal: 1.0,
|
|
675
|
-
min: capabilities.zoom.min,
|
|
676
|
-
max: Math.min(capabilities.zoom.max, 3.0)
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
// Add advanced constraints if any were set
|
|
680
|
-
if (Object.keys(advanced).length > 0) {
|
|
681
|
-
advancedConstraints.push(advanced);
|
|
682
|
-
}
|
|
683
|
-
if (advancedConstraints.length > 0) {
|
|
684
|
-
constraints.advanced = advancedConstraints;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
applyBasicFocusSettings(constraints) {
|
|
688
|
-
// Apply basic focus settings when capabilities are not available
|
|
689
|
-
// Use advanced constraints for non-standard properties
|
|
690
|
-
const advanced = {
|
|
691
|
-
focusMode: 'continuous',
|
|
692
|
-
exposureMode: 'continuous',
|
|
693
|
-
whiteBalanceMode: 'continuous'
|
|
694
|
-
};
|
|
695
|
-
if (!constraints.advanced) {
|
|
696
|
-
constraints.advanced = [];
|
|
697
|
-
}
|
|
698
|
-
constraints.advanced.push(advanced);
|
|
699
|
-
constraints.frameRate = { ideal: 30 };
|
|
700
|
-
}
|
|
701
|
-
getCapabilitiesSummary(capabilities) {
|
|
702
|
-
return {
|
|
703
|
-
hasAutoFocus: !!capabilities.focusMode,
|
|
704
|
-
hasFocusDistance: !!capabilities.focusDistance,
|
|
705
|
-
hasExposureControl: !!capabilities.exposureMode,
|
|
706
|
-
hasWhiteBalance: !!capabilities.whiteBalanceMode,
|
|
707
|
-
hasZoom: !!capabilities.zoom,
|
|
708
|
-
hasTorch: capabilities.torch !== undefined,
|
|
709
|
-
hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
|
|
710
|
-
resolutionRange: capabilities.width && capabilities.height ?
|
|
711
|
-
`${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
// New method to enable torch/flash for better illumination
|
|
715
|
-
async setTorchEnabled(enabled, stream) {
|
|
716
|
-
try {
|
|
717
|
-
if (!stream) {
|
|
718
|
-
this.logger.warn('No hay stream disponible para controlar el flash');
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
722
|
-
if (!videoTrack) {
|
|
723
|
-
this.logger.warn('No hay track de video disponible');
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
const capabilities = videoTrack.getCapabilities();
|
|
727
|
-
if (capabilities.torch === undefined) {
|
|
728
|
-
this.logger.warn('El dispositivo no soporta control de flash');
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
|
-
await videoTrack.applyConstraints({
|
|
732
|
-
advanced: [{ torch: enabled }]
|
|
733
|
-
});
|
|
734
|
-
this.logger.state('FLASH_CONTROLADO', { enabled });
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
catch (error) {
|
|
738
|
-
this.logger.error('Error al controlar el flash:', error);
|
|
539
|
+
// Optimization helper methods
|
|
540
|
+
constraintsEqual(a, b) {
|
|
541
|
+
if (!a || !b)
|
|
739
542
|
return false;
|
|
740
|
-
|
|
543
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
741
544
|
}
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (!videoTrack) {
|
|
751
|
-
this.logger.warn('No hay track de video disponible');
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
const capabilities = videoTrack.getCapabilities();
|
|
755
|
-
if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
|
|
756
|
-
this.logger.warn('El dispositivo no soporta enfoque manual');
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
// Trigger single-shot focus
|
|
760
|
-
await videoTrack.applyConstraints({
|
|
761
|
-
advanced: [{ focusMode: 'single-shot' }]
|
|
762
|
-
});
|
|
763
|
-
// Return to continuous focus after a short delay
|
|
764
|
-
setTimeout(async () => {
|
|
765
|
-
try {
|
|
766
|
-
await videoTrack.applyConstraints({
|
|
767
|
-
advanced: [{ focusMode: 'continuous' }]
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
catch (error) {
|
|
771
|
-
this.logger.warn('Error al volver a enfoque continuo:', error);
|
|
772
|
-
}
|
|
773
|
-
}, 1000);
|
|
774
|
-
this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
|
|
775
|
-
return true;
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
this.logger.error('Error al enfocar manualmente:', error);
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
545
|
+
// Cleanup method for optimization
|
|
546
|
+
static clearCaches() {
|
|
547
|
+
CameraService.cameraCapabilitiesCache.clear();
|
|
548
|
+
CameraService.deviceEnumerationCache = null;
|
|
549
|
+
}
|
|
550
|
+
// Method to invalidate device cache when needed
|
|
551
|
+
invalidateDeviceCache() {
|
|
552
|
+
CameraService.deviceEnumerationCache = null;
|
|
781
553
|
}
|
|
782
554
|
}
|
|
783
555
|
|
|
@@ -856,9 +628,9 @@ class DeviceStrategyFactory {
|
|
|
856
628
|
}
|
|
857
629
|
|
|
858
630
|
class DetectionService {
|
|
859
|
-
logger;
|
|
860
631
|
debug;
|
|
861
632
|
useDocumentClassification;
|
|
633
|
+
useDocumentDetector;
|
|
862
634
|
session;
|
|
863
635
|
mobileNetSession;
|
|
864
636
|
mobileNetClassMap;
|
|
@@ -873,30 +645,37 @@ class DetectionService {
|
|
|
873
645
|
preprocessCanvas;
|
|
874
646
|
preprocessCtx;
|
|
875
647
|
captureCanvas;
|
|
876
|
-
|
|
877
|
-
|
|
648
|
+
// Static canvas pool for reuse across instances
|
|
649
|
+
static canvasPool = new Map();
|
|
650
|
+
static MAX_POOL_SIZE = 5;
|
|
651
|
+
constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
|
|
878
652
|
this.debug = debug;
|
|
879
653
|
this.useDocumentClassification = useDocumentClassification;
|
|
654
|
+
this.useDocumentDetector = useDocumentDetector;
|
|
880
655
|
this.deviceStrategy = DeviceStrategyFactory.createStrategy();
|
|
881
656
|
this.initializeCanvasPool();
|
|
882
657
|
}
|
|
883
658
|
async loadModel() {
|
|
884
659
|
if (this.modelLoaded || this.session) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!this.useDocumentDetector) {
|
|
663
|
+
// Skip loading the detection model but mark as "loaded" for compatibility
|
|
664
|
+
this.modelLoaded = true;
|
|
889
665
|
return;
|
|
890
666
|
}
|
|
891
667
|
try {
|
|
892
|
-
this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
|
|
893
668
|
const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
|
|
894
669
|
try {
|
|
895
670
|
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
|
|
896
671
|
}
|
|
897
672
|
catch (error) {
|
|
898
|
-
|
|
899
|
-
|
|
673
|
+
// Múltiples tipos de errores de memoria que pueden ocurrir
|
|
674
|
+
const isMemoryError = error.message.includes('failed to allocate a buffer') ||
|
|
675
|
+
error.message.includes('Out of memory') ||
|
|
676
|
+
error.message.includes('RangeError: Out of memory') ||
|
|
677
|
+
error.message.includes('no available backend found');
|
|
678
|
+
if (isMemoryError) {
|
|
900
679
|
const fallbackOptions = {
|
|
901
680
|
executionProviders: ['wasm'],
|
|
902
681
|
graphOptimizationLevel: 'disabled',
|
|
@@ -906,18 +685,25 @@ class DetectionService {
|
|
|
906
685
|
executionMode: 'sequential',
|
|
907
686
|
interOpNumThreads: 1,
|
|
908
687
|
intraOpNumThreads: 1,
|
|
688
|
+
// Configuraciones adicionales para dispositivos con poca memoria
|
|
689
|
+
enableProfiling: false,
|
|
690
|
+
sessionLogSeverityLevel: 4,
|
|
691
|
+
sessionLogVerbosityLevel: 0,
|
|
909
692
|
};
|
|
910
|
-
|
|
693
|
+
try {
|
|
694
|
+
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, fallbackOptions);
|
|
695
|
+
}
|
|
696
|
+
catch (fallbackError) {
|
|
697
|
+
throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
|
|
698
|
+
}
|
|
911
699
|
}
|
|
912
700
|
else {
|
|
913
701
|
throw error;
|
|
914
702
|
}
|
|
915
703
|
}
|
|
916
704
|
this.modelLoaded = true;
|
|
917
|
-
this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
|
|
918
705
|
}
|
|
919
706
|
catch (error) {
|
|
920
|
-
this.logger.error('Error al precargar modelo de detección:', error);
|
|
921
707
|
throw error;
|
|
922
708
|
}
|
|
923
709
|
}
|
|
@@ -926,19 +712,14 @@ class DetectionService {
|
|
|
926
712
|
return;
|
|
927
713
|
}
|
|
928
714
|
try {
|
|
929
|
-
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
930
715
|
// Try to load class map (optional - SqueezeNet may not need it for basic classification)
|
|
931
716
|
try {
|
|
932
717
|
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
933
718
|
if (classResponse.ok) {
|
|
934
719
|
this.mobileNetClassMap = await classResponse.json();
|
|
935
|
-
this.logger.state('CLASES_MOBILENET_CARGADAS', {
|
|
936
|
-
classCount: Object.keys(this.mobileNetClassMap).length
|
|
937
|
-
});
|
|
938
720
|
}
|
|
939
721
|
}
|
|
940
722
|
catch (error) {
|
|
941
|
-
this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
|
|
942
723
|
// Create a basic class map for document types
|
|
943
724
|
this.mobileNetClassMap = {
|
|
944
725
|
"0": "document",
|
|
@@ -954,8 +735,12 @@ class DetectionService {
|
|
|
954
735
|
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, sessionOptions);
|
|
955
736
|
}
|
|
956
737
|
catch (error) {
|
|
957
|
-
|
|
958
|
-
|
|
738
|
+
// Múltiples tipos de errores de memoria que pueden ocurrir
|
|
739
|
+
const isMemoryError = error.message.includes('failed to allocate a buffer') ||
|
|
740
|
+
error.message.includes('Out of memory') ||
|
|
741
|
+
error.message.includes('RangeError: Out of memory') ||
|
|
742
|
+
error.message.includes('no available backend found');
|
|
743
|
+
if (isMemoryError) {
|
|
959
744
|
const fallbackOptions = {
|
|
960
745
|
executionProviders: ['wasm'],
|
|
961
746
|
graphOptimizationLevel: 'disabled',
|
|
@@ -965,23 +750,32 @@ class DetectionService {
|
|
|
965
750
|
executionMode: 'sequential',
|
|
966
751
|
interOpNumThreads: 1,
|
|
967
752
|
intraOpNumThreads: 1,
|
|
753
|
+
// Configuraciones adicionales para dispositivos con poca memoria
|
|
754
|
+
enableProfiling: false,
|
|
755
|
+
sessionLogSeverityLevel: 4,
|
|
756
|
+
sessionLogVerbosityLevel: 0,
|
|
968
757
|
};
|
|
969
|
-
|
|
758
|
+
try {
|
|
759
|
+
this.mobileNetSession = await window.ort.InferenceSession.create(this.MOBILENET_MODEL_PATH, fallbackOptions);
|
|
760
|
+
}
|
|
761
|
+
catch (fallbackError) {
|
|
762
|
+
throw new Error(`no available backend found. ERR: [wasm] RangeError: Out of memory`);
|
|
763
|
+
}
|
|
970
764
|
}
|
|
971
765
|
else {
|
|
972
766
|
throw error;
|
|
973
767
|
}
|
|
974
768
|
}
|
|
975
|
-
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
|
|
976
|
-
sessionCreated: !!this.mobileNetSession
|
|
977
|
-
});
|
|
978
769
|
}
|
|
979
770
|
catch (error) {
|
|
980
|
-
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
981
771
|
throw error;
|
|
982
772
|
}
|
|
983
773
|
}
|
|
984
774
|
isModelLoaded() {
|
|
775
|
+
if (!this.useDocumentDetector) {
|
|
776
|
+
// If detector is disabled, consider it "loaded" for compatibility
|
|
777
|
+
return this.modelLoaded;
|
|
778
|
+
}
|
|
985
779
|
return this.modelLoaded && !!this.session;
|
|
986
780
|
}
|
|
987
781
|
preprocess(video) {
|
|
@@ -1006,6 +800,10 @@ class DetectionService {
|
|
|
1006
800
|
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
1007
801
|
}
|
|
1008
802
|
async runInference(inputTensor) {
|
|
803
|
+
if (!this.useDocumentDetector) {
|
|
804
|
+
// Return empty array when document detector is disabled
|
|
805
|
+
return [];
|
|
806
|
+
}
|
|
1009
807
|
if (!this.session) {
|
|
1010
808
|
throw new Error('Detection model not loaded');
|
|
1011
809
|
}
|
|
@@ -1030,11 +828,9 @@ class DetectionService {
|
|
|
1030
828
|
}
|
|
1031
829
|
async classifyDocument(canvas) {
|
|
1032
830
|
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
1033
|
-
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
1034
831
|
return null;
|
|
1035
832
|
}
|
|
1036
833
|
try {
|
|
1037
|
-
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
1038
834
|
const inputTensor = this.preprocessMobileNet(canvas);
|
|
1039
835
|
const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
|
|
1040
836
|
const results = await this.mobileNetSession.run(feeds);
|
|
@@ -1042,13 +838,6 @@ class DetectionService {
|
|
|
1042
838
|
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
1043
839
|
const confidence = output[maxIdx];
|
|
1044
840
|
const className = this.mobileNetClassMap[maxIdx.toString()] || "unknown";
|
|
1045
|
-
this.logger.state('DOCUMENTO_CLASIFICADO', {
|
|
1046
|
-
class: className,
|
|
1047
|
-
confidence: confidence.toFixed(3),
|
|
1048
|
-
classIndex: maxIdx,
|
|
1049
|
-
timestamp: Date.now(),
|
|
1050
|
-
results
|
|
1051
|
-
});
|
|
1052
841
|
return {
|
|
1053
842
|
class: className,
|
|
1054
843
|
confidence: confidence,
|
|
@@ -1056,7 +845,6 @@ class DetectionService {
|
|
|
1056
845
|
};
|
|
1057
846
|
}
|
|
1058
847
|
catch (error) {
|
|
1059
|
-
this.logger.error('Error al clasificar documento:', error);
|
|
1060
848
|
return null;
|
|
1061
849
|
}
|
|
1062
850
|
}
|
|
@@ -1169,7 +957,6 @@ class DetectionService {
|
|
|
1169
957
|
}
|
|
1170
958
|
this.mobileNetClassMap = undefined;
|
|
1171
959
|
this.modelLoaded = false;
|
|
1172
|
-
this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
|
|
1173
960
|
}
|
|
1174
961
|
initializeCanvasPool() {
|
|
1175
962
|
this.preprocessCanvas = document.createElement('canvas');
|
|
@@ -1180,9 +967,6 @@ class DetectionService {
|
|
|
1180
967
|
willReadFrequently: true
|
|
1181
968
|
});
|
|
1182
969
|
this.captureCanvas = document.createElement('canvas');
|
|
1183
|
-
this.logger.state('CANVAS_POOL_INICIALIZADO', {
|
|
1184
|
-
preprocessCanvasSize: this.INPUT_SIZE
|
|
1185
|
-
});
|
|
1186
970
|
}
|
|
1187
971
|
cleanupCanvasPool() {
|
|
1188
972
|
if (this.preprocessCanvas) {
|
|
@@ -1218,10 +1002,11 @@ class DetectionService {
|
|
|
1218
1002
|
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1219
1003
|
}
|
|
1220
1004
|
preprocessMobileNet(canvas) {
|
|
1221
|
-
|
|
1222
|
-
tempCanvas
|
|
1223
|
-
tempCanvas.height = 224;
|
|
1005
|
+
// Use pooled canvas instead of creating new one
|
|
1006
|
+
const tempCanvas = this.getCanvas(224, 224);
|
|
1224
1007
|
const tempCtx = tempCanvas.getContext('2d');
|
|
1008
|
+
// Clear canvas before use
|
|
1009
|
+
tempCtx.clearRect(0, 0, 224, 224);
|
|
1225
1010
|
// Resize image to 224x224 for SqueezeNet (using MobileNet interface)
|
|
1226
1011
|
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
1227
1012
|
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
@@ -1237,8 +1022,53 @@ class DetectionService {
|
|
|
1237
1022
|
// Blue channel
|
|
1238
1023
|
arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
|
|
1239
1024
|
}
|
|
1025
|
+
// Return canvas to pool after use
|
|
1026
|
+
this.returnCanvas(tempCanvas);
|
|
1240
1027
|
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
1241
1028
|
}
|
|
1029
|
+
// Canvas pool management methods for optimization
|
|
1030
|
+
getCanvas(width, height) {
|
|
1031
|
+
const key = `${width}x${height}`;
|
|
1032
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1033
|
+
DetectionService.canvasPool.set(key, []);
|
|
1034
|
+
}
|
|
1035
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1036
|
+
let canvas = pool.pop();
|
|
1037
|
+
if (!canvas) {
|
|
1038
|
+
canvas = document.createElement('canvas');
|
|
1039
|
+
canvas.width = width;
|
|
1040
|
+
canvas.height = height;
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
// Clear the canvas before reuse
|
|
1044
|
+
const ctx = canvas.getContext('2d');
|
|
1045
|
+
ctx.clearRect(0, 0, width, height);
|
|
1046
|
+
}
|
|
1047
|
+
return canvas;
|
|
1048
|
+
}
|
|
1049
|
+
returnCanvas(canvas) {
|
|
1050
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
1051
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1052
|
+
DetectionService.canvasPool.set(key, []);
|
|
1053
|
+
}
|
|
1054
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1055
|
+
// Only return to pool if not at max capacity
|
|
1056
|
+
if (pool.length < DetectionService.MAX_POOL_SIZE) {
|
|
1057
|
+
pool.push(canvas);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Static method to clear all canvas pools (useful for memory management)
|
|
1061
|
+
static clearCanvasPools() {
|
|
1062
|
+
DetectionService.canvasPool.clear();
|
|
1063
|
+
}
|
|
1064
|
+
// Method to get pool statistics for debugging
|
|
1065
|
+
getPoolStats() {
|
|
1066
|
+
const stats = {};
|
|
1067
|
+
DetectionService.canvasPool.forEach((pool, key) => {
|
|
1068
|
+
stats[key] = pool.length;
|
|
1069
|
+
});
|
|
1070
|
+
return stats;
|
|
1071
|
+
}
|
|
1242
1072
|
}
|
|
1243
1073
|
|
|
1244
1074
|
class ServiceContainer {
|
|
@@ -1249,13 +1079,11 @@ class ServiceContainer {
|
|
|
1249
1079
|
initializeServices(config) {
|
|
1250
1080
|
// Initialize services in dependency order
|
|
1251
1081
|
const eventBus = new EventBusService();
|
|
1252
|
-
const logger = new LoggerService(config.debug);
|
|
1253
1082
|
const stateManager = new StateManagerService(eventBus);
|
|
1254
|
-
const cameraService = new CameraService(
|
|
1255
|
-
const detectionService = new DetectionService(
|
|
1083
|
+
const cameraService = new CameraService(eventBus, config.preferredCamera);
|
|
1084
|
+
const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
|
|
1256
1085
|
// Register services
|
|
1257
1086
|
this.services.set('eventBus', eventBus);
|
|
1258
|
-
this.services.set('logger', logger);
|
|
1259
1087
|
this.services.set('stateManager', stateManager);
|
|
1260
1088
|
this.services.set('cameraService', cameraService);
|
|
1261
1089
|
this.services.set('detectionService', detectionService);
|
|
@@ -1267,9 +1095,6 @@ class ServiceContainer {
|
|
|
1267
1095
|
}
|
|
1268
1096
|
return service;
|
|
1269
1097
|
}
|
|
1270
|
-
getLogger() {
|
|
1271
|
-
return this.get('logger');
|
|
1272
|
-
}
|
|
1273
1098
|
getEventBus() {
|
|
1274
1099
|
return this.get('eventBus');
|
|
1275
1100
|
}
|
|
@@ -1283,10 +1108,6 @@ class ServiceContainer {
|
|
|
1283
1108
|
return this.get('detectionService');
|
|
1284
1109
|
}
|
|
1285
1110
|
updateConfig(config) {
|
|
1286
|
-
if (config.debug !== undefined) {
|
|
1287
|
-
const logger = this.services.get('logger');
|
|
1288
|
-
logger.setDebugMode(config.debug);
|
|
1289
|
-
}
|
|
1290
1111
|
}
|
|
1291
1112
|
cleanup() {
|
|
1292
1113
|
// Cleanup services in reverse order
|
|
@@ -1296,7 +1117,7 @@ class ServiceContainer {
|
|
|
1296
1117
|
}
|
|
1297
1118
|
}
|
|
1298
1119
|
|
|
1299
|
-
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);color:#fff;font-size:14px;font-weight:600;text-align:center;white-space:normal;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);padding:12px 20px;border-radius:8px;max-width:300px;z-index:20;border:1px solid rgba(255, 255, 255, 0.1)}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.skip-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateY(1px)}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(255, 255, 255, 0.3);border-top:2px solid #ffffff;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:space-between;align-items:center;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:8px;font-weight:400}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}";
|
|
1120
|
+
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled,.capture-button.primary-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@media (max-width: 480px){.back-capture-section{bottom:16px;max-width:300px}.back-capture-buttons{flex-direction:column;gap:8px;width:100%}.capture-button,.back-capture-buttons .skip-button{width:100%;max-width:200px;padding:10px 20px;font-size:13px}.capture-button:disabled,.capture-button.primary-button:disabled,.skip-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}.skip-explanation{background:rgba(0, 0, 0, 0.5);color:#ffffff;padding:10px 16px;border-radius:8px;font-size:14px;font-weight:500;text-align:center;margin-bottom:12px;box-shadow:0 2px 8px rgba(0, 0, 0, 0.2);backdrop-filter:blur(12px);border:1px solid rgba(255, 255, 255, 0.1);animation:fadeIn 0.3s ease-in-out}@keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}.guide-text.performance-warning-text{animation:gentlePulse 2s ease-in-out infinite;background:rgba(255, 107, 107, 0.3);border:1px solid rgba(255, 107, 107, 0.5)}@keyframes gentlePulse{0%,100%{transform:translate(-50%, -50%) scale(1);background:rgba(255, 107, 107, 0.3)}50%{transform:translate(-50%, -50%) scale(1.02);background:rgba(255, 107, 107, 0.4)}}.skip-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.skip-button:hover{background:#f8f9fa}.skip-button:active{background:#e9ecef;transform:translateY(1px)}.skip-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}.camera-controls{position:absolute;top:16px;right:16px;z-index:25;display:flex;gap:8px;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}.camera-selector-button{height:32px;padding:0 10px;border:none;border-radius:8px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(12px);color:#ffffff;font-size:12px;font-weight:500;cursor:pointer;transition:all 0.2s ease;display:flex;align-items:center;justify-content:center;border:1px solid rgba(255, 255, 255, 0.1);white-space:nowrap;min-width:fit-content}.camera-selector-button:hover{background:rgba(0, 0, 0, 0.8);border-color:rgba(255, 255, 255, 0.2);transform:translateY(-1px)}.camera-selector-button:active{transform:translateY(0);background:rgba(0, 0, 0, 0.9)}.camera-selector-button:disabled,.camera-selector-button.loading{opacity:0.7;cursor:not-allowed;pointer-events:none}.camera-selector-button:disabled:hover,.camera-selector-button.loading:hover{transform:none;background:rgba(0, 0, 0, 0.6);border-color:rgba(255, 255, 255, 0.1)}.button-spinner{width:16px;height:16px;border:2px solid rgba(0, 0, 0, 0.2);border-top:2px solid #333333;border-radius:50%;animation:spin 1s linear infinite;display:inline-block;margin-right:8px;vertical-align:middle}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.camera-selector-dropdown{position:absolute;top:56px;right:16px;z-index:30;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;min-width:260px;max-width:300px;border:1px solid rgba(255, 255, 255, 0.1);overflow:hidden;pointer-events:auto;animation:slideInFromTop 0.3s ease-out}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-8px) scale(0.95)}100%{opacity:1;transform:translateY(0) scale(1)}}@keyframes slideInFromTopCentered{0%{opacity:0;transform:translate(-50%, -50%) translateY(-8px) scale(0.95)}100%{opacity:1;transform:translate(-50%, -50%) translateY(0) scale(1)}}.camera-selector-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;border-bottom:1px solid rgba(255, 255, 255, 0.1)}.camera-selector-header span{font-weight:500;color:#ffffff;font-size:13px;opacity:0.9}.close-selector{width:20px;height:20px;border:none;background:none;color:rgba(255, 255, 255, 0.7);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:all 0.2s ease}.close-selector:hover{background:rgba(255, 255, 255, 0.1);color:#ffffff}.camera-list{padding:4px 0;max-height:240px;overflow-y:auto}.camera-option{width:100%;padding:8px 12px;border:none;background:none;text-align:left;cursor:pointer;transition:all 0.2s ease;display:flex;justify-content:flex-start;align-items:center;gap:8px;color:rgba(255, 255, 255, 0.9)}.camera-option:hover{background:rgba(255, 255, 255, 0.08)}.camera-option.selected{background:rgba(255, 255, 255, 0.12);color:#ffffff}.camera-label{font-size:13px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-right:0;font-weight:400;display:flex;align-items:center;gap:6px}.camera-label .autofocus-icon{font-size:12px;color:#ffffff !important;opacity:0.8;flex-shrink:0}.selected-indicator{font-size:14px;color:#ffffff;opacity:0.9;flex-shrink:0}.device-info{padding:6px 12px;border-top:1px solid rgba(255, 255, 255, 0.1);background:rgba(255, 255, 255, 0.05)}.device-info small{color:rgba(255, 255, 255, 0.6);font-size:11px;text-transform:capitalize;font-weight:400}@media (max-width: 480px){.camera-controls{top:12px;right:12px;gap:6px}.camera-selector-button{height:36px;padding:0 12px;font-size:11px;border-radius:6px}.camera-selector-dropdown{right:12px;top:48px;min-width:240px;max-width:calc(100vw - 24px)}.camera-selector-header{padding:6px 10px}.camera-option{padding:6px 10px}.device-info{padding:4px 10px}}.watermark{position:absolute;bottom:12px;right:12px;z-index:15;pointer-events:none;opacity:0.7}.watermark img{height:24px;width:auto;filter:drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3))}.component-status{position:absolute;top:16px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);display:flex;align-items:center;gap:6px;padding:6px 10px;z-index:40;height:32px;width:fit-content;animation:slideInFromTop 0.3s ease-out}.status-spinner{width:16px;height:16px;border:1px solid rgba(255, 255, 255, 0.3);border-top:1px solid #ffffff;border-radius:50%;animation:statusSpin 1s linear infinite;flex-shrink:0}.status-content{display:flex;flex-direction:column;gap:1px}.status-message{color:#ffffff;font-size:12px;font-weight:500;margin:0}.status-description{color:rgba(255, 255, 255, 0.7);font-size:10px;line-height:1.2;margin:0}@keyframes statusSpin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@media (max-width: 480px){.component-status{top:12px;left:12px;padding:4px 8px;gap:4px;height:28px;border-radius:8px}.status-spinner{width:14px;height:14px}.status-message{font-size:11px}.status-description{font-size:9px}.guide-text{font-size:11px;padding:4px 8px;border-radius:8px}}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0, 0, 0, 0.85);backdrop-filter:blur(4px);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:30;border-radius:8px}.loading-spinner{width:50px;height:50px;border:3px solid rgba(255, 255, 255, 0.15);border-top:3px solid #28a745;border-radius:50%;animation:spin 1s ease-in-out infinite;margin-bottom:16px}.loading-text{color:#ffffff;font-size:14px;font-weight:500;text-align:center;opacity:0.9;margin-bottom:4px}.loading-description{color:rgba(255, 255, 255, 0.7);font-size:12px;text-align:center;opacity:0.8}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.performance-monitor{position:absolute;top:70px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromTop 0.3s ease-out;transition:all 0.3s ease}@keyframes slideInFromLeft{0%{opacity:0;transform:translateX(-20px) scale(0.95)}100%{opacity:1;transform:translateX(0) scale(1)}}.performance-expanded{padding:6px 10px}.metrics-row{display:flex;gap:8px;margin-bottom:4px}.metrics-row:last-child{margin-bottom:0}.metric-compact{display:flex;flex-direction:column;align-items:center;gap:2px;min-width:50px}.metric-label{font-size:9px;color:rgba(255, 255, 255, 0.6);font-weight:500;text-transform:uppercase;letter-spacing:0.3px}.metric-value{font-size:10px;font-weight:600;font-family:'Monaco', 'Menlo', 'Consolas', monospace;padding:2px 4px;border-radius:3px;text-align:center;white-space:nowrap}.metric-value.good{color:#4ade80;background:rgba(74, 222, 128, 0.1);border:1px solid rgba(74, 222, 128, 0.2)}.metric-value.warning{color:#fbbf24;background:rgba(251, 191, 36, 0.1);border:1px solid rgba(251, 191, 36, 0.2)}.metric-value.danger{color:#f87171;background:rgba(248, 113, 113, 0.1);border:1px solid rgba(248, 113, 113, 0.2)}@media (max-width: 480px){.performance-monitor{top:60px;left:12px;width:fit-content}.performance-expanded{padding:4px 8px}.metrics-row{gap:6px;margin-bottom:3px}.metric-compact{min-width:45px;gap:1px}.metric-label{font-size:8px}.metric-value{font-size:9px;padding:1px 3px}}.quality-monitor{position:absolute;top:200px;left:16px;background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);z-index:35;width:fit-content;animation:slideInFromLeft 0.3s ease-out;transition:all 0.3s ease}.quality-text{padding:6px 10px;font-size:11px;color:white;font-family:'Monaco', 'Menlo', 'Consolas', monospace;line-height:1.4}.quality-fail{color:#ff9999}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.7}}.metric-value.danger{animation:pulse 2s infinite}@media (max-width: 480px){.quality-monitor{top:160px;left:12px}.quality-text{padding:4px 8px;font-size:10px}}.manual-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.manual-capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.manual-capture-button:hover{background:#f8f9fa}.manual-capture-button:active{background:#e9ecef;transform:translateY(1px)}.manual-capture-button:disabled{cursor:not-allowed !important;transform:none !important;opacity:0.7 !important;transition:none !important}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}.manual-capture-button:disabled{opacity:0.7 !important;cursor:not-allowed !important}}";
|
|
1300
1121
|
|
|
1301
1122
|
const JaakStamps = class {
|
|
1302
1123
|
constructor(hostRef) {
|
|
@@ -1310,6 +1131,7 @@ const JaakStamps = class {
|
|
|
1310
1131
|
maskSize = 90;
|
|
1311
1132
|
cropMargin = 20;
|
|
1312
1133
|
useDocumentClassification = false;
|
|
1134
|
+
useDocumentDetector = true;
|
|
1313
1135
|
preferredCamera = 'auto';
|
|
1314
1136
|
captureDelay = 1500;
|
|
1315
1137
|
enableBackDocumentTimer = false;
|
|
@@ -1326,9 +1148,16 @@ const JaakStamps = class {
|
|
|
1326
1148
|
showCameraSelector = false;
|
|
1327
1149
|
isSwitchingCamera = false;
|
|
1328
1150
|
hasDocumentDetected = false;
|
|
1151
|
+
cameraInfoWithAutofocus = {
|
|
1152
|
+
availableCameras: [],
|
|
1153
|
+
selectedCameraId: null,
|
|
1154
|
+
deviceType: 'desktop',
|
|
1155
|
+
isMultipleCamerasAvailable: false,
|
|
1156
|
+
preferredFacing: null
|
|
1157
|
+
};
|
|
1329
1158
|
currentStatus = {
|
|
1330
|
-
message: '
|
|
1331
|
-
description: 'Configurando
|
|
1159
|
+
message: 'Preparando capturador...',
|
|
1160
|
+
description: 'Configurando herramientas de captura',
|
|
1332
1161
|
type: 'initializing',
|
|
1333
1162
|
isInitialized: false
|
|
1334
1163
|
};
|
|
@@ -1343,9 +1172,13 @@ const JaakStamps = class {
|
|
|
1343
1172
|
detectionRate: 0
|
|
1344
1173
|
};
|
|
1345
1174
|
backDocumentTimerRemaining = 0;
|
|
1175
|
+
showManualCaptureButton = false;
|
|
1176
|
+
performanceDegradedMode = false; // Auto-switched to manual mode due to performance
|
|
1177
|
+
showPerformanceMessage = false; // Show performance message temporarily
|
|
1178
|
+
captureStateVersion = 0; // Triggers re-render when StateManager changes
|
|
1179
|
+
processingButton = null; // Track which button is processing
|
|
1346
1180
|
// Services
|
|
1347
1181
|
serviceContainer;
|
|
1348
|
-
logger;
|
|
1349
1182
|
eventBus;
|
|
1350
1183
|
stateManager;
|
|
1351
1184
|
cameraService;
|
|
@@ -1362,12 +1195,12 @@ const JaakStamps = class {
|
|
|
1362
1195
|
alignmentStartTime;
|
|
1363
1196
|
alignmentTimer;
|
|
1364
1197
|
backDocumentTimer;
|
|
1198
|
+
orientationTimer;
|
|
1365
1199
|
// Performance monitoring
|
|
1366
1200
|
performanceMetrics = {
|
|
1367
1201
|
fps: 0,
|
|
1368
1202
|
inferenceTime: 0,
|
|
1369
1203
|
memoryUsage: 0,
|
|
1370
|
-
cpuUsage: 0,
|
|
1371
1204
|
onnxLoadTime: 0,
|
|
1372
1205
|
frameProcessingTime: 0,
|
|
1373
1206
|
totalDetections: 0,
|
|
@@ -1376,22 +1209,39 @@ const JaakStamps = class {
|
|
|
1376
1209
|
lastUpdateTime: 0
|
|
1377
1210
|
};
|
|
1378
1211
|
performanceUpdateInterval;
|
|
1379
|
-
// Performance optimization
|
|
1212
|
+
// Performance optimization with adaptive frame skipping
|
|
1380
1213
|
frameSkipCounter = 0;
|
|
1381
|
-
|
|
1214
|
+
BASE_FRAME_SKIP = 2;
|
|
1215
|
+
MAX_FRAME_SKIP = 8;
|
|
1382
1216
|
consecutiveFailures = 0;
|
|
1383
1217
|
MAX_FAILURES = 30;
|
|
1384
1218
|
lastInferenceTime = 0;
|
|
1385
1219
|
MIN_INFERENCE_INTERVAL = 50;
|
|
1220
|
+
performanceHistory = [];
|
|
1221
|
+
PERFORMANCE_HISTORY_SIZE = 10;
|
|
1222
|
+
// Sistema simplificado de monitoreo de rendimiento
|
|
1223
|
+
PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
|
|
1224
|
+
SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
|
|
1225
|
+
slowFrameCount = 0; // Contador de frames consecutivos lentos
|
|
1226
|
+
processedFramesCount = 0; // Contador total de frames procesados
|
|
1227
|
+
hasAutoSwitchedToManual = false;
|
|
1228
|
+
// Canvas pool for optimized screenshot capture
|
|
1229
|
+
canvasPool = [];
|
|
1230
|
+
MAX_CANVAS_POOL_SIZE = 3;
|
|
1386
1231
|
async componentDidLoad() {
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
this.
|
|
1232
|
+
try {
|
|
1233
|
+
this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
|
|
1234
|
+
await this.initializeServices();
|
|
1235
|
+
this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
|
|
1236
|
+
await this.setupEventListeners();
|
|
1237
|
+
this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1238
|
+
await this.initializeComponent();
|
|
1239
|
+
if (this.debug) {
|
|
1240
|
+
this.initializePerformanceMonitor();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
this.updateStatus('Error de inicialización', error.message, 'error');
|
|
1395
1245
|
}
|
|
1396
1246
|
}
|
|
1397
1247
|
async initializeServices() {
|
|
@@ -1401,37 +1251,22 @@ const JaakStamps = class {
|
|
|
1401
1251
|
maskSize: this.maskSize,
|
|
1402
1252
|
cropMargin: this.cropMargin,
|
|
1403
1253
|
useDocumentClassification: this.useDocumentClassification,
|
|
1254
|
+
useDocumentDetector: this.useDocumentDetector,
|
|
1404
1255
|
preferredCamera: this.preferredCamera,
|
|
1405
1256
|
captureDelay: this.captureDelay
|
|
1406
1257
|
};
|
|
1407
1258
|
this.serviceContainer = new ServiceContainer(config);
|
|
1408
|
-
this.logger = this.serviceContainer.getLogger();
|
|
1409
1259
|
this.eventBus = this.serviceContainer.getEventBus();
|
|
1410
1260
|
this.stateManager = this.serviceContainer.getStateManager();
|
|
1411
1261
|
this.cameraService = this.serviceContainer.getCameraService();
|
|
1412
1262
|
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1413
|
-
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1414
1263
|
}
|
|
1415
1264
|
async setupEventListeners() {
|
|
1416
|
-
this.eventBus.on('state-changed', (
|
|
1417
|
-
this.handleStateChange(
|
|
1418
|
-
});
|
|
1419
|
-
this.eventBus.on('camera-changed', (cameraId) => {
|
|
1420
|
-
this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
|
|
1421
|
-
});
|
|
1422
|
-
this.eventBus.on('error', (error) => {
|
|
1423
|
-
this.logger.error('Error en servicio:', error);
|
|
1265
|
+
this.eventBus.on('state-changed', () => {
|
|
1266
|
+
this.handleStateChange();
|
|
1424
1267
|
});
|
|
1425
1268
|
}
|
|
1426
1269
|
async initializeComponent() {
|
|
1427
|
-
this.logger.state('COMPONENTE_INICIALIZANDO', {
|
|
1428
|
-
debug: this.debug,
|
|
1429
|
-
maskSize: this.maskSize,
|
|
1430
|
-
cropMargin: this.cropMargin,
|
|
1431
|
-
useDocumentClassification: this.useDocumentClassification,
|
|
1432
|
-
preferredCamera: this.preferredCamera,
|
|
1433
|
-
captureDelay: this.captureDelay
|
|
1434
|
-
});
|
|
1435
1270
|
this.validateProps();
|
|
1436
1271
|
if (this.debug) {
|
|
1437
1272
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
@@ -1440,58 +1275,67 @@ const JaakStamps = class {
|
|
|
1440
1275
|
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1441
1276
|
await this.cameraService.detectDeviceType();
|
|
1442
1277
|
await this.cameraService.enumerateDevices();
|
|
1443
|
-
this.
|
|
1278
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1279
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
|
|
1444
1280
|
await this.loadOnnxRuntime();
|
|
1445
1281
|
this.initializeResizeObserver();
|
|
1446
1282
|
}
|
|
1447
1283
|
validateProps() {
|
|
1448
1284
|
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
1449
|
-
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
1450
1285
|
this.maskSize = 90;
|
|
1451
1286
|
}
|
|
1452
1287
|
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
1453
|
-
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
1454
1288
|
this.cropMargin = 0;
|
|
1455
1289
|
}
|
|
1456
1290
|
const validOptions = ['auto', 'front', 'back'];
|
|
1457
1291
|
if (!validOptions.includes(this.preferredCamera)) {
|
|
1458
|
-
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
1459
1292
|
this.preferredCamera = 'auto';
|
|
1460
1293
|
}
|
|
1461
1294
|
if (this.captureDelay < 0 || this.captureDelay > 10000) {
|
|
1462
|
-
this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
|
|
1463
1295
|
this.captureDelay = 1500;
|
|
1464
1296
|
}
|
|
1465
1297
|
}
|
|
1466
1298
|
async loadOnnxRuntime() {
|
|
1467
1299
|
if (!window.ort) {
|
|
1468
|
-
this.updateStatus('
|
|
1300
|
+
this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
|
|
1469
1301
|
const script = document.createElement('script');
|
|
1470
1302
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
1471
1303
|
document.head.appendChild(script);
|
|
1472
1304
|
await new Promise((resolve) => {
|
|
1473
1305
|
script.onload = () => {
|
|
1474
|
-
setTimeout(() => {
|
|
1475
|
-
this.finalizeInitialization();
|
|
1306
|
+
setTimeout(async () => {
|
|
1307
|
+
await this.finalizeInitialization();
|
|
1476
1308
|
resolve(undefined);
|
|
1477
1309
|
}, 300);
|
|
1478
1310
|
};
|
|
1479
1311
|
});
|
|
1480
1312
|
}
|
|
1481
1313
|
else {
|
|
1482
|
-
setTimeout(() => {
|
|
1483
|
-
this.finalizeInitialization();
|
|
1314
|
+
setTimeout(async () => {
|
|
1315
|
+
await this.finalizeInitialization();
|
|
1484
1316
|
}, 300);
|
|
1485
1317
|
}
|
|
1486
1318
|
}
|
|
1487
|
-
finalizeInitialization() {
|
|
1319
|
+
async finalizeInitialization() {
|
|
1488
1320
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1321
|
+
// Check camera permissions before showing "ready" status
|
|
1322
|
+
const hasPermissions = await this.checkCameraPermissions();
|
|
1323
|
+
if (hasPermissions) {
|
|
1324
|
+
this.currentStatus = {
|
|
1325
|
+
message: 'Listo para capturar',
|
|
1326
|
+
description: '',
|
|
1327
|
+
type: 'ready',
|
|
1328
|
+
isInitialized: true
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
this.currentStatus = {
|
|
1333
|
+
message: 'Permisos de cámara requeridos',
|
|
1334
|
+
description: 'Por favor, otorgue permisos de cámara para continuar',
|
|
1335
|
+
type: 'error',
|
|
1336
|
+
isInitialized: true
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1495
1339
|
this.emitReadyEvent();
|
|
1496
1340
|
}
|
|
1497
1341
|
updateStatus(message, description, type = 'loading') {
|
|
@@ -1505,13 +1349,29 @@ const JaakStamps = class {
|
|
|
1505
1349
|
emitReadyEvent() {
|
|
1506
1350
|
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1507
1351
|
this.isReady.emit(isDocumentReady);
|
|
1508
|
-
this.logger.state('COMPONENTE_LISTO', {
|
|
1509
|
-
ortLibraryLoaded: !!window.ort,
|
|
1510
|
-
modelPreloaded: this.detectionService.isModelLoaded(),
|
|
1511
|
-
isReady: isDocumentReady
|
|
1512
|
-
});
|
|
1513
1352
|
}
|
|
1514
|
-
|
|
1353
|
+
async checkCameraPermissions() {
|
|
1354
|
+
try {
|
|
1355
|
+
if (!navigator.permissions) {
|
|
1356
|
+
// Fallback: try to access camera directly
|
|
1357
|
+
try {
|
|
1358
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
1359
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
return false;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
1367
|
+
return permission.state === 'granted';
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
handleStateChange() {
|
|
1374
|
+
// Handle state changes from StateManager if needed
|
|
1515
1375
|
}
|
|
1516
1376
|
initializeResizeObserver() {
|
|
1517
1377
|
if ('ResizeObserver' in window && this.detectionContainer) {
|
|
@@ -1520,15 +1380,56 @@ const JaakStamps = class {
|
|
|
1520
1380
|
});
|
|
1521
1381
|
resizeObserver.observe(this.detectionContainer.parentElement);
|
|
1522
1382
|
}
|
|
1383
|
+
// Handle mobile device orientation changes
|
|
1384
|
+
window.addEventListener('orientationchange', () => {
|
|
1385
|
+
// Clear any existing orientation timer
|
|
1386
|
+
if (this.orientationTimer) {
|
|
1387
|
+
clearTimeout(this.orientationTimer);
|
|
1388
|
+
}
|
|
1389
|
+
// Add delay to ensure video dimensions are updated after rotation
|
|
1390
|
+
this.orientationTimer = window.setTimeout(() => {
|
|
1391
|
+
this.handleResize();
|
|
1392
|
+
this.orientationTimer = undefined;
|
|
1393
|
+
}, 300);
|
|
1394
|
+
});
|
|
1523
1395
|
}
|
|
1524
1396
|
handleResize() {
|
|
1525
1397
|
if (this.detectionContainer) {
|
|
1526
1398
|
const container = this.detectionContainer.parentElement;
|
|
1527
1399
|
const rect = container.getBoundingClientRect();
|
|
1528
1400
|
this.updateMaskDimensions(rect);
|
|
1529
|
-
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
1530
1401
|
}
|
|
1531
1402
|
}
|
|
1403
|
+
triggerRerender() {
|
|
1404
|
+
this.captureStateVersion = this.captureStateVersion + 1;
|
|
1405
|
+
}
|
|
1406
|
+
getGuideText(step) {
|
|
1407
|
+
if (step === 'completed') {
|
|
1408
|
+
return 'Proceso completado';
|
|
1409
|
+
}
|
|
1410
|
+
// Check if we should show performance message
|
|
1411
|
+
if (this.showPerformanceMessage && step === 'front') {
|
|
1412
|
+
return 'Modo manual activado por rendimiento. Use el botón para capturar.';
|
|
1413
|
+
}
|
|
1414
|
+
if (step === 'front') {
|
|
1415
|
+
// Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
|
|
1416
|
+
if (this.showManualCaptureButton) {
|
|
1417
|
+
return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
|
|
1418
|
+
}
|
|
1419
|
+
// Instrucción básica de alineación
|
|
1420
|
+
return 'Alinee su identificación con el marco';
|
|
1421
|
+
}
|
|
1422
|
+
else if (step === 'back') {
|
|
1423
|
+
// Para el reverso, mostrar instrucciones específicas según el modo
|
|
1424
|
+
if (this.useDocumentDetector) {
|
|
1425
|
+
return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return 'Alinee su identificación con el marco';
|
|
1432
|
+
}
|
|
1532
1433
|
updateMaskDimensions(containerRect) {
|
|
1533
1434
|
if (!this.videoRef)
|
|
1534
1435
|
return;
|
|
@@ -1580,16 +1481,38 @@ const JaakStamps = class {
|
|
|
1580
1481
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
1581
1482
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
1582
1483
|
this.isMaskReady = true;
|
|
1583
|
-
this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
|
|
1584
|
-
video: { width: videoWidth, height: videoHeight },
|
|
1585
|
-
displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
|
|
1586
|
-
mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
|
|
1587
|
-
center: { x: videoCenterXPercent, y: videoCenterYPercent },
|
|
1588
|
-
offset: { x: videoOffsetX, y: videoOffsetY }
|
|
1589
|
-
});
|
|
1590
1484
|
}
|
|
1591
1485
|
// PUBLIC METHODS
|
|
1486
|
+
// Helper method to check if component is ready for operations
|
|
1487
|
+
isComponentReady() {
|
|
1488
|
+
if (!this.currentStatus.isInitialized) {
|
|
1489
|
+
return {
|
|
1490
|
+
ready: false,
|
|
1491
|
+
message: 'El componente aún se está inicializando',
|
|
1492
|
+
status: 'initializing'
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
|
|
1496
|
+
return {
|
|
1497
|
+
ready: false,
|
|
1498
|
+
message: 'El componente está cargando recursos',
|
|
1499
|
+
status: 'loading'
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
|
|
1503
|
+
return {
|
|
1504
|
+
ready: false,
|
|
1505
|
+
message: 'Los servicios del componente no están disponibles',
|
|
1506
|
+
status: 'error'
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
return { ready: true };
|
|
1510
|
+
}
|
|
1592
1511
|
async getCapturedImages() {
|
|
1512
|
+
const readyCheck = this.isComponentReady();
|
|
1513
|
+
if (!readyCheck.ready) {
|
|
1514
|
+
throw new Error(readyCheck.message);
|
|
1515
|
+
}
|
|
1593
1516
|
const state = this.stateManager.getCaptureState();
|
|
1594
1517
|
if (state.step !== 'completed') {
|
|
1595
1518
|
throw new Error('El proceso de captura no ha sido completado');
|
|
@@ -1597,146 +1520,359 @@ const JaakStamps = class {
|
|
|
1597
1520
|
return this.stateManager.getCapturedImages();
|
|
1598
1521
|
}
|
|
1599
1522
|
async isProcessCompleted() {
|
|
1523
|
+
const readyCheck = this.isComponentReady();
|
|
1524
|
+
if (!readyCheck.ready) {
|
|
1525
|
+
return false; // Return false instead of error for this method
|
|
1526
|
+
}
|
|
1600
1527
|
return this.stateManager.isProcessCompleted();
|
|
1601
1528
|
}
|
|
1602
1529
|
async startCapture() {
|
|
1603
|
-
|
|
1530
|
+
const readyCheck = this.isComponentReady();
|
|
1531
|
+
if (!readyCheck.ready) {
|
|
1532
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1533
|
+
return { success: false, error: readyCheck.message };
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
await this.startDetection();
|
|
1537
|
+
return { success: true };
|
|
1538
|
+
}
|
|
1539
|
+
catch (error) {
|
|
1540
|
+
this.updateStatus('Error al iniciar captura', error.message, 'error');
|
|
1541
|
+
return { success: false, error: error.message };
|
|
1542
|
+
}
|
|
1604
1543
|
}
|
|
1605
1544
|
async stopCapture() {
|
|
1606
|
-
this.
|
|
1545
|
+
const readyCheck = this.isComponentReady();
|
|
1546
|
+
if (!readyCheck.ready) {
|
|
1547
|
+
return { success: false, error: readyCheck.message };
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
this.exitSession();
|
|
1551
|
+
return { success: true };
|
|
1552
|
+
}
|
|
1553
|
+
catch (error) {
|
|
1554
|
+
return { success: false, error: error.message };
|
|
1555
|
+
}
|
|
1607
1556
|
}
|
|
1608
1557
|
async resetCapture() {
|
|
1609
|
-
this.
|
|
1558
|
+
const readyCheck = this.isComponentReady();
|
|
1559
|
+
if (!readyCheck.ready) {
|
|
1560
|
+
return { success: false, error: readyCheck.message };
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
this.resetDetection();
|
|
1564
|
+
return { success: true };
|
|
1565
|
+
}
|
|
1566
|
+
catch (error) {
|
|
1567
|
+
return { success: false, error: error.message };
|
|
1568
|
+
}
|
|
1610
1569
|
}
|
|
1611
1570
|
async skipBackCapture() {
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1571
|
+
console.log('🟡 Botón clicked: Saltar reverso');
|
|
1572
|
+
// Set processing state immediately
|
|
1573
|
+
this.processingButton = 'skip-back';
|
|
1574
|
+
// Add small delay to show spinner
|
|
1575
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1576
|
+
const readyCheck = this.isComponentReady();
|
|
1577
|
+
if (!readyCheck.ready) {
|
|
1578
|
+
this.processingButton = null;
|
|
1579
|
+
return { success: false, error: readyCheck.message };
|
|
1616
1580
|
}
|
|
1617
|
-
|
|
1581
|
+
try {
|
|
1582
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1583
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1584
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1585
|
+
this.completeProcess(true);
|
|
1586
|
+
return { success: true };
|
|
1587
|
+
}
|
|
1588
|
+
else {
|
|
1589
|
+
return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
return { success: false, error: error.message };
|
|
1594
|
+
}
|
|
1595
|
+
finally {
|
|
1596
|
+
// Always clear processing state when done
|
|
1597
|
+
this.processingButton = null;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1618
1600
|
async getStatus() {
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1601
|
+
const readyCheck = this.isComponentReady();
|
|
1602
|
+
if (!readyCheck.ready) {
|
|
1603
|
+
return {
|
|
1604
|
+
isVideoActive: false,
|
|
1605
|
+
captureStep: 'front',
|
|
1606
|
+
hasImages: false,
|
|
1607
|
+
isProcessCompleted: false,
|
|
1608
|
+
isModelPreloaded: false,
|
|
1609
|
+
componentStatus: readyCheck.status,
|
|
1610
|
+
componentMessage: readyCheck.message
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1615
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1616
|
+
return {
|
|
1617
|
+
isVideoActive: captureState.isVideoActive,
|
|
1618
|
+
captureStep: captureState.step,
|
|
1619
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1620
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1621
|
+
isModelPreloaded: this.detectionService.isModelLoaded(),
|
|
1622
|
+
componentStatus: 'ready'
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
catch (error) {
|
|
1626
|
+
return {
|
|
1627
|
+
isVideoActive: false,
|
|
1628
|
+
captureStep: 'front',
|
|
1629
|
+
hasImages: false,
|
|
1630
|
+
isProcessCompleted: false,
|
|
1631
|
+
isModelPreloaded: false,
|
|
1632
|
+
componentStatus: 'error',
|
|
1633
|
+
componentMessage: error.message
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1628
1636
|
}
|
|
1629
1637
|
async preloadModel() {
|
|
1638
|
+
const readyCheck = this.isComponentReady();
|
|
1639
|
+
if (!readyCheck.ready) {
|
|
1640
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1641
|
+
return { success: false, error: readyCheck.message };
|
|
1642
|
+
}
|
|
1630
1643
|
if (this.detectionService.isModelLoaded()) {
|
|
1631
|
-
this.
|
|
1632
|
-
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1644
|
+
this.updateStatus('Reconocimiento listo', '', 'ready');
|
|
1633
1645
|
return { success: true, message: 'Model already loaded' };
|
|
1634
1646
|
}
|
|
1635
1647
|
try {
|
|
1636
1648
|
const loadStartTime = performance.now();
|
|
1637
|
-
this.updateStatus('
|
|
1638
|
-
this.stateManager
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1649
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1650
|
+
if (this.stateManager) {
|
|
1651
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
await this.detectionService.loadModel();
|
|
1655
|
+
}
|
|
1656
|
+
catch (modelError) {
|
|
1657
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1658
|
+
throw modelError;
|
|
1659
|
+
}
|
|
1660
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1661
|
+
try {
|
|
1662
|
+
await this.detectionService.loadClassificationModel();
|
|
1663
|
+
}
|
|
1664
|
+
catch (classificationError) {
|
|
1665
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1666
|
+
throw classificationError;
|
|
1667
|
+
}
|
|
1642
1668
|
const loadEndTime = performance.now();
|
|
1643
1669
|
const loadTime = loadEndTime - loadStartTime;
|
|
1644
1670
|
// Record ONNX load time
|
|
1645
1671
|
if (this.debug) {
|
|
1646
1672
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1647
1673
|
}
|
|
1648
|
-
this.updateStatus('
|
|
1674
|
+
this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
|
|
1649
1675
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1650
|
-
this.updateStatus('
|
|
1651
|
-
this.stateManager
|
|
1676
|
+
this.updateStatus('Reconocimiento preparado', '', 'ready');
|
|
1677
|
+
if (this.stateManager) {
|
|
1678
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1679
|
+
}
|
|
1652
1680
|
this.emitReadyEvent();
|
|
1653
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1654
1681
|
return { success: true, message: 'Models preloaded successfully' };
|
|
1655
1682
|
}
|
|
1656
1683
|
catch (error) {
|
|
1657
|
-
this.
|
|
1658
|
-
this.
|
|
1659
|
-
|
|
1684
|
+
this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
|
|
1685
|
+
if (this.stateManager) {
|
|
1686
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1687
|
+
}
|
|
1660
1688
|
return { success: false, error: error.message };
|
|
1661
1689
|
}
|
|
1662
1690
|
}
|
|
1663
1691
|
async getCameraInfo() {
|
|
1664
|
-
|
|
1692
|
+
const readyCheck = this.isComponentReady();
|
|
1693
|
+
if (!readyCheck.ready) {
|
|
1694
|
+
return {
|
|
1695
|
+
availableCameras: [],
|
|
1696
|
+
selectedCameraId: null,
|
|
1697
|
+
deviceType: 'desktop',
|
|
1698
|
+
isMultipleCamerasAvailable: false,
|
|
1699
|
+
preferredFacing: null,
|
|
1700
|
+
error: readyCheck.message
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
try {
|
|
1704
|
+
return await this.cameraService.getCameraInfo();
|
|
1705
|
+
}
|
|
1706
|
+
catch (error) {
|
|
1707
|
+
return {
|
|
1708
|
+
availableCameras: [],
|
|
1709
|
+
selectedCameraId: null,
|
|
1710
|
+
deviceType: 'desktop',
|
|
1711
|
+
isMultipleCamerasAvailable: false,
|
|
1712
|
+
preferredFacing: null,
|
|
1713
|
+
error: error.message
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1665
1716
|
}
|
|
1666
1717
|
async setPreferredCamera(camera) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1718
|
+
const readyCheck = this.isComponentReady();
|
|
1719
|
+
if (!readyCheck.ready) {
|
|
1720
|
+
return {
|
|
1721
|
+
success: false,
|
|
1722
|
+
error: readyCheck.message,
|
|
1723
|
+
selectedCamera: null,
|
|
1724
|
+
availableCameras: 0
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
try {
|
|
1728
|
+
this.preferredCamera = camera;
|
|
1729
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1730
|
+
await this.cameraService.enumerateDevices();
|
|
1731
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1732
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1733
|
+
if (captureState.isVideoActive) {
|
|
1734
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1735
|
+
if (selectedCameraId) {
|
|
1736
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1737
|
+
}
|
|
1675
1738
|
}
|
|
1739
|
+
return {
|
|
1740
|
+
success: true,
|
|
1741
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1742
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
catch (error) {
|
|
1746
|
+
return {
|
|
1747
|
+
success: false,
|
|
1748
|
+
error: error.message,
|
|
1749
|
+
selectedCamera: null,
|
|
1750
|
+
availableCameras: 0
|
|
1751
|
+
};
|
|
1676
1752
|
}
|
|
1677
|
-
return {
|
|
1678
|
-
success: true,
|
|
1679
|
-
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1680
|
-
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1681
|
-
};
|
|
1682
1753
|
}
|
|
1683
1754
|
async setCaptureDelay(delay) {
|
|
1684
1755
|
if (delay < 0 || delay > 10000) {
|
|
1685
|
-
|
|
1756
|
+
return {
|
|
1757
|
+
success: false,
|
|
1758
|
+
error: 'Capture delay must be between 0 and 10000 milliseconds',
|
|
1759
|
+
captureDelay: this.captureDelay
|
|
1760
|
+
};
|
|
1686
1761
|
}
|
|
1762
|
+
// Allow setting capture delay even during initialization
|
|
1687
1763
|
this.captureDelay = delay;
|
|
1688
|
-
this.serviceContainer
|
|
1764
|
+
if (this.serviceContainer) {
|
|
1765
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1766
|
+
}
|
|
1689
1767
|
return {
|
|
1690
1768
|
success: true,
|
|
1691
1769
|
captureDelay: this.captureDelay
|
|
1692
1770
|
};
|
|
1693
1771
|
}
|
|
1694
1772
|
async getCaptureDelay() {
|
|
1773
|
+
// Always allow getting capture delay, even during initialization
|
|
1695
1774
|
return this.captureDelay;
|
|
1696
1775
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
};
|
|
1703
|
-
}
|
|
1704
|
-
async focusAtPoint(x, y) {
|
|
1705
|
-
const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
|
|
1776
|
+
// Memory and device capability detection
|
|
1777
|
+
checkDeviceCapabilities() {
|
|
1778
|
+
const deviceMemory = navigator.deviceMemory;
|
|
1779
|
+
// Siempre intentar usar ML, independientemente del dispositivo
|
|
1780
|
+
// Las restricciones anteriores no eran confiables
|
|
1706
1781
|
return {
|
|
1707
|
-
|
|
1708
|
-
|
|
1782
|
+
canUseML: true,
|
|
1783
|
+
reason: 'Dispositivo compatible con detección automática.',
|
|
1784
|
+
memoryMB: deviceMemory ? deviceMemory * 1024 : null
|
|
1709
1785
|
};
|
|
1710
1786
|
}
|
|
1711
1787
|
// DETECTION METHODS
|
|
1712
1788
|
async startDetection() {
|
|
1713
|
-
this.logger.state('INICIANDO_DETECCION');
|
|
1714
1789
|
try {
|
|
1790
|
+
// Verificar capacidades del dispositivo antes de cargar modelos
|
|
1791
|
+
const capabilities = this.checkDeviceCapabilities();
|
|
1792
|
+
// Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
|
|
1793
|
+
if (!capabilities.canUseML && this.useDocumentDetector) {
|
|
1794
|
+
this.switchToManualModeWithWarning(capabilities.reason);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1715
1797
|
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1716
1798
|
if (!this.detectionService.isModelLoaded()) {
|
|
1717
1799
|
const loadStartTime = performance.now();
|
|
1718
|
-
this.updateStatus('
|
|
1800
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1719
1801
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1802
|
+
try {
|
|
1803
|
+
await this.detectionService.loadModel();
|
|
1804
|
+
}
|
|
1805
|
+
catch (modelError) {
|
|
1806
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1807
|
+
if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
|
|
1808
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1809
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1814
|
+
throw modelError;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1818
|
+
try {
|
|
1819
|
+
await this.detectionService.loadClassificationModel();
|
|
1820
|
+
}
|
|
1821
|
+
catch (classificationError) {
|
|
1822
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1823
|
+
if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
|
|
1824
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1825
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
else {
|
|
1829
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1830
|
+
throw classificationError;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1723
1833
|
const loadEndTime = performance.now();
|
|
1724
1834
|
const loadTime = loadEndTime - loadStartTime;
|
|
1725
1835
|
// Record ONNX load time
|
|
1726
1836
|
if (this.debug) {
|
|
1727
1837
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1728
1838
|
}
|
|
1729
|
-
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
1730
1839
|
}
|
|
1731
1840
|
// Paso 2: Detectar y configurar dispositivos
|
|
1732
|
-
this.updateStatus('
|
|
1733
|
-
|
|
1841
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
1842
|
+
try {
|
|
1843
|
+
await this.cameraService.enumerateDevices();
|
|
1844
|
+
}
|
|
1845
|
+
catch (enumerateError) {
|
|
1846
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1850
|
+
}
|
|
1851
|
+
catch (cameraInfoError) {
|
|
1852
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
1853
|
+
}
|
|
1734
1854
|
// Paso 3: Configurar cámara seleccionada
|
|
1735
|
-
this.updateStatus('
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1855
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
1856
|
+
let stream;
|
|
1857
|
+
try {
|
|
1858
|
+
stream = await this.cameraService.setupCamera();
|
|
1859
|
+
}
|
|
1860
|
+
catch (setupError) {
|
|
1861
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
1862
|
+
}
|
|
1863
|
+
// Paso 4: Inicializar video y mostrar estado de análisis inicial
|
|
1864
|
+
if (this.useDocumentDetector) {
|
|
1865
|
+
this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
|
|
1866
|
+
}
|
|
1867
|
+
else {
|
|
1868
|
+
this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
|
|
1869
|
+
}
|
|
1870
|
+
try {
|
|
1871
|
+
await this.initializeVideoStream(stream);
|
|
1872
|
+
}
|
|
1873
|
+
catch (videoError) {
|
|
1874
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
1875
|
+
}
|
|
1740
1876
|
// Paso 5: Calibrar máscara
|
|
1741
1877
|
if (this.detectionContainer) {
|
|
1742
1878
|
const container = this.detectionContainer.parentElement;
|
|
@@ -1746,11 +1882,14 @@ const JaakStamps = class {
|
|
|
1746
1882
|
// Finalizar e iniciar captura
|
|
1747
1883
|
this.startTime = Date.now();
|
|
1748
1884
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1885
|
+
// Mostrar botón manual si el detector está deshabilitado
|
|
1886
|
+
if (!this.useDocumentDetector) {
|
|
1887
|
+
this.showManualCaptureButton = true;
|
|
1888
|
+
}
|
|
1749
1889
|
this.detectFrame();
|
|
1750
1890
|
}
|
|
1751
1891
|
catch (err) {
|
|
1752
|
-
this.
|
|
1753
|
-
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1892
|
+
this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
|
|
1754
1893
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1755
1894
|
}
|
|
1756
1895
|
}
|
|
@@ -1760,14 +1899,16 @@ const JaakStamps = class {
|
|
|
1760
1899
|
this.videoStream = stream;
|
|
1761
1900
|
const isRear = this.cameraService.isRearCamera(stream);
|
|
1762
1901
|
this.shouldMirrorVideo = !isRear;
|
|
1763
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1764
|
-
isRearCamera: isRear,
|
|
1765
|
-
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1766
|
-
});
|
|
1767
1902
|
return new Promise((resolve) => {
|
|
1768
1903
|
this.videoRef.onloadedmetadata = async () => {
|
|
1769
1904
|
await this.videoRef.play();
|
|
1770
1905
|
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
1906
|
+
// Recalculate mask dimensions when new camera stream loads
|
|
1907
|
+
if (this.detectionContainer) {
|
|
1908
|
+
const container = this.detectionContainer.parentElement;
|
|
1909
|
+
const rect = container.getBoundingClientRect();
|
|
1910
|
+
this.updateMaskDimensions(rect);
|
|
1911
|
+
}
|
|
1771
1912
|
resolve();
|
|
1772
1913
|
};
|
|
1773
1914
|
});
|
|
@@ -1782,15 +1923,28 @@ const JaakStamps = class {
|
|
|
1782
1923
|
const captureState = this.stateManager.getCaptureState();
|
|
1783
1924
|
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1784
1925
|
return;
|
|
1926
|
+
// Show manual capture button when document detector is disabled or performance is degraded
|
|
1927
|
+
if (!this.useDocumentDetector || this.performanceDegradedMode) {
|
|
1928
|
+
this.showManualCaptureButton = true;
|
|
1929
|
+
// Continue the loop for UI updates but skip detection logic
|
|
1930
|
+
if (captureState.step !== 'completed') {
|
|
1931
|
+
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1932
|
+
}
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
else if (!this.performanceDegradedMode) {
|
|
1936
|
+
this.showManualCaptureButton = false;
|
|
1937
|
+
}
|
|
1785
1938
|
if (captureState.isDetectionPaused) {
|
|
1786
1939
|
if (captureState.step !== 'completed') {
|
|
1787
1940
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1788
1941
|
}
|
|
1789
1942
|
return;
|
|
1790
1943
|
}
|
|
1791
|
-
//
|
|
1944
|
+
// Adaptive frame skipping for performance
|
|
1792
1945
|
this.frameSkipCounter++;
|
|
1793
|
-
|
|
1946
|
+
const currentFrameSkip = this.getAdaptiveFrameSkip();
|
|
1947
|
+
if (this.frameSkipCounter <= currentFrameSkip) {
|
|
1794
1948
|
if (captureState.step !== 'completed') {
|
|
1795
1949
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1796
1950
|
}
|
|
@@ -1808,10 +1962,27 @@ const JaakStamps = class {
|
|
|
1808
1962
|
this.lastInferenceTime = currentTime;
|
|
1809
1963
|
// Measure preprocessing and inference time
|
|
1810
1964
|
const inferenceStartTime = performance.now();
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1965
|
+
let inputTensor;
|
|
1966
|
+
let detections;
|
|
1967
|
+
let inferenceTime;
|
|
1968
|
+
try {
|
|
1969
|
+
inputTensor = this.detectionService.preprocess(this.videoRef);
|
|
1970
|
+
}
|
|
1971
|
+
catch (preprocessError) {
|
|
1972
|
+
this.switchToManualModeWithError('Error al procesar imagen');
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
try {
|
|
1976
|
+
// Standard detection without timeout (timeout is handled by frame counting)
|
|
1977
|
+
detections = await this.detectionService.runInference(inputTensor);
|
|
1978
|
+
inferenceTime = performance.now() - inferenceStartTime;
|
|
1979
|
+
}
|
|
1980
|
+
catch (inferenceError) {
|
|
1981
|
+
this.switchToManualModeWithError('Error en detección automática');
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
// Increment frame count
|
|
1985
|
+
this.processedFramesCount++;
|
|
1815
1986
|
// Record ONNX performance metrics
|
|
1816
1987
|
if (this.debug) {
|
|
1817
1988
|
this.recordOnnxPerformance(0, inferenceTime);
|
|
@@ -1850,9 +2021,30 @@ const JaakStamps = class {
|
|
|
1850
2021
|
}
|
|
1851
2022
|
this.updateMaskColor(detections);
|
|
1852
2023
|
// Record frame processing metrics
|
|
2024
|
+
const frameEndTime = performance.now();
|
|
2025
|
+
const totalFrameTime = frameEndTime - frameStartTime;
|
|
2026
|
+
// Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
|
|
2027
|
+
if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
|
|
2028
|
+
// Verificar si el frame es lento
|
|
2029
|
+
if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
|
|
2030
|
+
this.slowFrameCount++;
|
|
2031
|
+
// Si tenemos suficientes frames lentos consecutivos, cambiar a manual
|
|
2032
|
+
if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
|
|
2033
|
+
this.switchToManualMode();
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
else {
|
|
2037
|
+
// Frame rápido, reiniciar contador
|
|
2038
|
+
if (this.slowFrameCount > 0) {
|
|
2039
|
+
this.slowFrameCount = 0;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// Cambiar mensaje de estado después de analizar los primeros 2 frames
|
|
2043
|
+
if (this.processedFramesCount === 2) {
|
|
2044
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1853
2047
|
if (this.debug) {
|
|
1854
|
-
const frameEndTime = performance.now();
|
|
1855
|
-
const totalFrameTime = frameEndTime - frameStartTime;
|
|
1856
2048
|
this.recordFrameProcessing(totalFrameTime, detections.length);
|
|
1857
2049
|
}
|
|
1858
2050
|
// Continue detection loop
|
|
@@ -1866,7 +2058,6 @@ const JaakStamps = class {
|
|
|
1866
2058
|
}
|
|
1867
2059
|
}
|
|
1868
2060
|
catch (e) {
|
|
1869
|
-
this.logger.error('Error en inferencia de modelo:', e);
|
|
1870
2061
|
const captureState = this.stateManager.getCaptureState();
|
|
1871
2062
|
if (captureState.step !== 'completed') {
|
|
1872
2063
|
setTimeout(() => this.detectFrame(), 100);
|
|
@@ -1889,10 +2080,18 @@ const JaakStamps = class {
|
|
|
1889
2080
|
clearTimeout(this.alignmentTimer);
|
|
1890
2081
|
this.alignmentTimer = undefined;
|
|
1891
2082
|
}
|
|
2083
|
+
if (this.orientationTimer) {
|
|
2084
|
+
clearTimeout(this.orientationTimer);
|
|
2085
|
+
this.orientationTimer = undefined;
|
|
2086
|
+
}
|
|
2087
|
+
// Liberar recursos de ONNX al limpiar el componente
|
|
2088
|
+
this.releaseOnnxResources();
|
|
1892
2089
|
if (this.performanceUpdateInterval) {
|
|
1893
2090
|
clearInterval(this.performanceUpdateInterval);
|
|
1894
2091
|
this.performanceUpdateInterval = undefined;
|
|
1895
2092
|
}
|
|
2093
|
+
// Clear canvas pool
|
|
2094
|
+
this.canvasPool = [];
|
|
1896
2095
|
this.detectionBoxes = [];
|
|
1897
2096
|
this.alignmentStartTime = undefined;
|
|
1898
2097
|
this.hasDocumentDetected = false;
|
|
@@ -1906,11 +2105,8 @@ const JaakStamps = class {
|
|
|
1906
2105
|
step: 'front',
|
|
1907
2106
|
isCapturing: false
|
|
1908
2107
|
};
|
|
1909
|
-
const cameraInfo = this.
|
|
1910
|
-
|
|
1911
|
-
selectedCameraId: null,
|
|
1912
|
-
deviceType: 'desktop'};
|
|
1913
|
-
return (h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, h("video", { key: 'a34f0481da20ec5b7b62b21aa275ec5f6dfec5d1', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
|
|
2108
|
+
const cameraInfo = this.cameraInfoWithAutofocus;
|
|
2109
|
+
return (h("div", { key: 'dda3e3054a18dac59fd6dab4bf722f405d1c1b0f', class: "detector-container" }, h("div", { key: 'c260bf52307e072a634cddcc273c1f55037d01ef', class: "video-container" }, h("video", { key: '5f20ce9d95c5ece7ab90a2da30f28034b86a57a0', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '6afd8a46c16592c67e6bb93f1655de1660338f30', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
|
|
1914
2110
|
position: 'absolute',
|
|
1915
2111
|
left: `${box.x}px`,
|
|
1916
2112
|
top: `${box.y}px`,
|
|
@@ -1919,9 +2115,9 @@ const JaakStamps = class {
|
|
|
1919
2115
|
border: '2px solid #32406C',
|
|
1920
2116
|
pointerEvents: 'none',
|
|
1921
2117
|
boxSizing: 'border-box'
|
|
1922
|
-
} })))), this.isMaskReady && (h("div", { key: '
|
|
2118
|
+
} })))), this.isMaskReady && (h("div", { key: '178f2022bed334f8c9c8c866a92b44c8cb0369b1', class: "overlay-mask" }, h("div", { key: 'acb1bc996409095afd29ffa5a038c3eeae6c7ed9', class: "card-outline" }, h("div", { key: 'dcc952d9011c5fd13b229f3a759cc74721a1ee11', class: "side side-top" }), h("div", { key: 'addb25f6c41ad10a09b6918e6357052294d14adf', class: "side side-right" }), h("div", { key: 'a1ec08f3bf1b16731a224c034c76dd8c6412597b', class: "side side-bottom" }), h("div", { key: '22f3c158f2c2bb1dc582f796c39ea60bc11505dd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '5b150f7df5b0c5402e42bae4601936d4e5d1f5d4', class: `guide-text ${this.showPerformanceMessage ? 'performance-warning-text' : ''}` }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '17b0a81437eb8691cb4dc5042c72bda01ac92be0', class: "back-capture-section" }, h("div", { key: 'a65ccbd5f53b06ec340b36a0a2348cf04920b3df', class: "back-capture-buttons" }, (!this.useDocumentDetector || this.performanceDegradedMode) && (h("button", { key: '383d7398079dbb8b1adfe695407cc598059b2a94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-back' && (h("span", { key: '86bddcfeb6f90da9b18926f4c6e1208296502dcf', class: "button-spinner" })), "Capturar Reverso")), h("button", { key: '5c9d116cbb9b5bc16481b2b90c8451182ad436c5', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'skip-back' && (h("span", { key: '881f00cfc3a24167bc6b8da5849cb404101b692d', class: "button-spinner" })), this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
|
|
1923
2119
|
? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
|
|
1924
|
-
: 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: '
|
|
2120
|
+
: 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: 'd929fc0eeeeee944f3d722fde98c52642443e9a5', class: "camera-controls" }, h("button", { key: '1afcfae9d4a23aaede153f27ba6895a33182ada1', class: `camera-selector-button ${this.isSwitchingCamera ? 'loading' : ''}`, onClick: () => this.toggleCameraSelector(), type: "button", title: "Seleccionar c\u00E1mara", disabled: this.isSwitchingCamera }, this.isSwitchingCamera ? (h("div", { class: "button-spinner" })) : ('Cámaras')))), this.showCameraSelector && cameraInfo.availableCameras.length > 0 && (h("div", { key: 'f37feefd9def935417467dffb52b9fe8d8acea79', class: "camera-selector-dropdown" }, h("div", { key: 'a8479ec6a6c4076b5dc40677e1988295100e4d0d', class: "camera-selector-header" }, h("span", { key: '074f02967354293a1e5df4e3ac8a126faee33d04' }, "Seleccionar C\u00E1mara"), h("button", { key: 'd6ba0252fb269c3a04967fb0e5c075166f202d71', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '63f5f9be2b05da71f4fe5a4e38b11ab3e39fb1e0', class: "camera-list" }, cameraInfo.availableCameras.map((camera) => (h("button", { key: camera.id, class: `camera-option ${cameraInfo.selectedCameraId === camera.id ? 'selected' : ''}`, onClick: () => this.handleCameraSwitch(camera.id), type: "button" }, h("span", { class: "camera-label" }, camera.label || `Cámara ${cameraInfo.availableCameras.indexOf(camera) + 1}`, camera.hasAutofocus && (h("span", { class: "autofocus-icon", title: "Autofoco disponible" }, "\u29BF"))), cameraInfo.selectedCameraId === camera.id && (h("span", { class: "selected-indicator" }, "\u2713")))))), h("div", { key: '4ca46fdebe0eaa078f1dae4d2f1af03321577c7f', class: "device-info" }, h("small", { key: '10c54ddc5a196b11a69077b045119187ed09f4be' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '25393018948798f4d9d5f44738e61c7a0a6be990', class: "manual-capture-section" }, h("button", { key: '7a6c8686c0db3e352f9689bc41f7fd3f612c800c', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: this.processingButton !== null }, this.processingButton === 'capture-front' && (h("span", { key: '1916b97da3f8dce7d06b86b21f0361dd7ce10b95', class: "button-spinner" })), "Capturar Frente"))))), captureState.isCapturing && (h("div", { key: 'aed05a3280dc96121564ffea87945f441d4286f6', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '63d262e5e10325d145fada8e91a878c47305f2b9', class: "flip-animation" }, h("div", { key: '2a3699f47e595c5e4ab00dfd11e1e66ab2e13039', class: "id-card-icon" }), h("div", { key: 'df195fe9a6db4ec3e14b04b4e932d55adb7c3f3c', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '670341c302a31c8017a9083ebf35e2988122ac3f', class: "success-animation" }, h("div", { key: '27b0b0d91de6a882182b6ccdebecc7c440997e36', class: "check-icon" }), h("div", { key: '71ee5327faf48ea26c2ae36a8a6906c448c3a389', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '39c92aa0b82f6169058b1092894845fcba3f27b4', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: '74b02ba81756f0e8bfb6e2feafdb2c44fb80afad', class: "status-spinner" })), h("div", { key: 'c7419efb2c6e2760019ecce619534e24ae3d7351', class: "status-content" }, h("div", { key: '57f315384630f17f6332eddfea628b333299e831', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'ac8632fdd68c24ed00192cb04a13026ada1f953d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: 'e51286a839795b18cfac2874e7e1f6e9f8e3bb4b', class: "performance-monitor" }, h("div", { key: 'f8cd289d260af9df2daf23bdfb8db70fe8bf6f45', class: "performance-expanded" }, h("div", { key: 'bf2becb2ee3ad3bffb82b0e90246a4385a19f567', class: "metrics-row" }, h("div", { key: 'f334c2b5de45df73aec6e06cd371151110789a45', class: "metric-compact" }, h("span", { key: 'cbf32f72eb3aaf1bf770d651c32123a43b81035c', class: "metric-label" }, "FPS"), h("span", { key: '9d2d2f46ba3fdb23c010f781044f00b1dd1250e6', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'f29406e8a5b00f5838a144de9fda77ce53177d43', class: "metric-compact" }, h("span", { key: '8f44e29f6134c340f0a58f8aa321612c8a9bbfb8', class: "metric-label" }, "MEM"), h("span", { key: '7a58e164d292329d95054b2679a90e031f6fe4c2', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'd0284a0e65fc2879557d73fc2e03295c29f512ae', class: "metrics-row" }, h("div", { key: '78dcbce552e65c95fcd01158f3aa8916ef0e7175', class: "metric-compact" }, h("span", { key: '673cfbc8c9dabb020a3d3c18ee07558a61da9b51', class: "metric-label" }, "INF"), h("span", { key: '833761b7a875c931a8b31913930c3b132f71f618', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: '324801e3e69a8de181755367201d8e8c643e8d72', class: "metric-compact" }, h("span", { key: 'f928d9b2dfbf81f5738ab08a42ebb76ff4a483de', class: "metric-label" }, "FRAME"), h("span", { key: 'dc9f2e4a1e848be4d240d89c89126203555ae203', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '7ca0a7b44719b1884334edab586267f021cf86c1', class: "metrics-row" }, h("div", { key: '262667c4320d337dae2220b77f142cf293542562', class: "metric-compact" }, h("span", { key: '578b53d251e58933f654a5afe558931e4333f022', class: "metric-label" }, "DET"), h("span", { key: '4fb36eaebebd73e5cb06d582fa3b4ece61463761', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'c17192e3964164ad603427de3befa1615b027022', class: "metric-compact" }, h("span", { key: 'd004e74797b9468e94c2af996bb62c1a3ca8d58e', class: "metric-label" }, "RATE"), h("span", { key: '0960e8760fd0ac2456a9e76598e5392c24e42d3b', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '3588fba1fe51af1335d643107ba0355aca6f4f8c', class: "watermark" }, h("img", { key: 'd53ec6b0ee9f97f602f4f8857ec605427b2ee478', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
1925
2121
|
}
|
|
1926
2122
|
// Utility methods
|
|
1927
2123
|
updateDetectionBoxes(boxes) {
|
|
@@ -2002,35 +2198,18 @@ const JaakStamps = class {
|
|
|
2002
2198
|
if (allSidesAligned && bestBox) {
|
|
2003
2199
|
cardOutline?.classList.add('perfect-match');
|
|
2004
2200
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
2005
|
-
// Debug logging
|
|
2006
|
-
this.logger.state('CAPTURE_EVALUATION', {
|
|
2007
|
-
allSidesAligned: true,
|
|
2008
|
-
hasScreenshotTaken: this.hasScreenshotTaken,
|
|
2009
|
-
captureDelay: this.captureDelay,
|
|
2010
|
-
alignmentStartTime: this.alignmentStartTime
|
|
2011
|
-
});
|
|
2012
2201
|
if (!this.hasScreenshotTaken) {
|
|
2013
2202
|
const currentTime = Date.now();
|
|
2014
2203
|
// Initialize alignment start time if not set
|
|
2015
2204
|
if (!this.alignmentStartTime) {
|
|
2016
2205
|
this.alignmentStartTime = currentTime;
|
|
2017
|
-
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
2018
2206
|
}
|
|
2019
2207
|
// Check if document has been aligned for the configured delay
|
|
2020
2208
|
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
2021
|
-
this.logger.state('ALIGNMENT_DURATION_CHECK', {
|
|
2022
|
-
alignmentDuration,
|
|
2023
|
-
captureDelay: this.captureDelay,
|
|
2024
|
-
readyToCapture: alignmentDuration >= this.captureDelay
|
|
2025
|
-
});
|
|
2026
2209
|
if (alignmentDuration >= this.captureDelay) {
|
|
2027
|
-
this.logger.state('TRIGGERING_CAPTURE', {
|
|
2028
|
-
alignmentDuration,
|
|
2029
|
-
captureDelay: this.captureDelay
|
|
2030
|
-
});
|
|
2031
2210
|
this.lastDetectedBox = bestBox;
|
|
2032
|
-
this.takeScreenshot().catch(
|
|
2033
|
-
|
|
2211
|
+
this.takeScreenshot().catch(() => {
|
|
2212
|
+
// Handle screenshot error silently
|
|
2034
2213
|
});
|
|
2035
2214
|
this.hasScreenshotTaken = true;
|
|
2036
2215
|
this.alignmentStartTime = undefined;
|
|
@@ -2053,24 +2232,150 @@ const JaakStamps = class {
|
|
|
2053
2232
|
}
|
|
2054
2233
|
}
|
|
2055
2234
|
}
|
|
2235
|
+
async takeManualScreenshot() {
|
|
2236
|
+
console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
|
|
2237
|
+
// Set processing state immediately
|
|
2238
|
+
const captureState = this.stateManager?.getCaptureState();
|
|
2239
|
+
if (captureState?.step === 'front') {
|
|
2240
|
+
this.processingButton = 'capture-front';
|
|
2241
|
+
}
|
|
2242
|
+
else if (captureState?.step === 'back') {
|
|
2243
|
+
this.processingButton = 'capture-back';
|
|
2244
|
+
}
|
|
2245
|
+
console.log('🔵 processingButton set to:', this.processingButton);
|
|
2246
|
+
// Add small delay to show spinner
|
|
2247
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2248
|
+
if (!this.videoRef) {
|
|
2249
|
+
this.processingButton = null;
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
// When using manual capture, use mask coordinates for cropping
|
|
2253
|
+
// Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
|
|
2254
|
+
await this.takeScreenshotWithMaskCoordinates();
|
|
2255
|
+
}
|
|
2256
|
+
async takeScreenshotWithMaskCoordinates() {
|
|
2257
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
2258
|
+
return;
|
|
2259
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2260
|
+
this.triggerCaptureAnimation();
|
|
2261
|
+
// Use pooled canvas for optimization
|
|
2262
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2263
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2264
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2265
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2266
|
+
// Calculate mask coordinates for cropping
|
|
2267
|
+
const container = this.detectionContainer.parentElement;
|
|
2268
|
+
const containerRect = container.getBoundingClientRect();
|
|
2269
|
+
// Get mask dimensions from CSS properties
|
|
2270
|
+
const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
|
|
2271
|
+
const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
|
|
2272
|
+
const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
|
|
2273
|
+
const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
|
|
2274
|
+
// Convert percentages to actual video coordinates
|
|
2275
|
+
const videoWidth = this.videoRef.videoWidth;
|
|
2276
|
+
const videoHeight = this.videoRef.videoHeight;
|
|
2277
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
2278
|
+
const containerAspectRatio = containerRect.width / containerRect.height;
|
|
2279
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
2280
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
2281
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
2282
|
+
displayedVideoWidth = containerRect.width;
|
|
2283
|
+
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
2284
|
+
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
2285
|
+
}
|
|
2286
|
+
else {
|
|
2287
|
+
displayedVideoHeight = containerRect.height;
|
|
2288
|
+
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
2289
|
+
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
2290
|
+
}
|
|
2291
|
+
// Calculate mask coordinates in video space
|
|
2292
|
+
const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
|
|
2293
|
+
const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
|
|
2294
|
+
const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
|
|
2295
|
+
const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
|
|
2296
|
+
// Convert to video coordinates
|
|
2297
|
+
const scaleX = videoWidth / displayedVideoWidth;
|
|
2298
|
+
const scaleY = videoHeight / displayedVideoHeight;
|
|
2299
|
+
const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
|
|
2300
|
+
const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
|
|
2301
|
+
const maskWidthInVideo = maskWidthInContainer * scaleX;
|
|
2302
|
+
const maskHeightInVideo = maskHeightInContainer * scaleY;
|
|
2303
|
+
// Calculate crop coordinates
|
|
2304
|
+
const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
|
|
2305
|
+
const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
|
|
2306
|
+
const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
|
|
2307
|
+
const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
|
|
2308
|
+
// Use pooled canvas for cropped version
|
|
2309
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2310
|
+
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2311
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2312
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2313
|
+
const captureState = this.stateManager.getCaptureState();
|
|
2314
|
+
if (captureState.step === 'front') {
|
|
2315
|
+
this.stateManager.setCapturedImages({
|
|
2316
|
+
front: {
|
|
2317
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2318
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
// Check if document classification is enabled (independent of detector)
|
|
2322
|
+
if (this.useDocumentClassification) {
|
|
2323
|
+
try {
|
|
2324
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2325
|
+
if (classification && classification.class === 'passport') {
|
|
2326
|
+
this.completeProcess(true);
|
|
2327
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2328
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2329
|
+
// Clear processing button when passport is detected
|
|
2330
|
+
this.processingButton = null;
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
catch (classifyError) {
|
|
2335
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
this.stateManager.updateCaptureState({
|
|
2339
|
+
step: 'back',
|
|
2340
|
+
isDetectionPaused: true,
|
|
2341
|
+
showFlipAnimation: true
|
|
2342
|
+
});
|
|
2343
|
+
this.triggerRerender();
|
|
2344
|
+
setTimeout(() => {
|
|
2345
|
+
this.stateManager.updateCaptureState({
|
|
2346
|
+
showFlipAnimation: false,
|
|
2347
|
+
isDetectionPaused: false
|
|
2348
|
+
});
|
|
2349
|
+
this.triggerRerender();
|
|
2350
|
+
this.startBackDocumentTimer();
|
|
2351
|
+
// Clear processing button after animation completes
|
|
2352
|
+
this.processingButton = null;
|
|
2353
|
+
}, 3000);
|
|
2354
|
+
}
|
|
2355
|
+
else if (captureState.step === 'back') {
|
|
2356
|
+
this.stateManager.setCapturedImages({
|
|
2357
|
+
back: {
|
|
2358
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2359
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2360
|
+
}
|
|
2361
|
+
});
|
|
2362
|
+
this.completeProcess(false);
|
|
2363
|
+
// Clear processing button after back capture completes
|
|
2364
|
+
this.processingButton = null;
|
|
2365
|
+
}
|
|
2366
|
+
// Return canvases to pool after use
|
|
2367
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2368
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2369
|
+
}
|
|
2056
2370
|
async takeScreenshot() {
|
|
2057
2371
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
2058
2372
|
return;
|
|
2059
|
-
this.logger.state('INICIANDO_CAPTURA', {
|
|
2060
|
-
captureStep: this.stateManager.getCaptureState().step,
|
|
2061
|
-
detectedBox: this.lastDetectedBox,
|
|
2062
|
-
videoResolution: {
|
|
2063
|
-
width: this.videoRef.videoWidth,
|
|
2064
|
-
height: this.videoRef.videoHeight
|
|
2065
|
-
}
|
|
2066
|
-
});
|
|
2067
2373
|
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2068
2374
|
this.triggerCaptureAnimation();
|
|
2069
|
-
//
|
|
2070
|
-
const captureCanvas =
|
|
2071
|
-
captureCanvas.width = this.videoRef.videoWidth;
|
|
2072
|
-
captureCanvas.height = this.videoRef.videoHeight;
|
|
2375
|
+
// Use pooled canvas for optimization
|
|
2376
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2073
2377
|
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2378
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2074
2379
|
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2075
2380
|
// Calculate crop coordinates
|
|
2076
2381
|
const INPUT_SIZE = 320;
|
|
@@ -2080,11 +2385,10 @@ const JaakStamps = class {
|
|
|
2080
2385
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
2081
2386
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
2082
2387
|
const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
|
|
2083
|
-
//
|
|
2084
|
-
const croppedCanvas =
|
|
2085
|
-
croppedCanvas.width = cropWidth;
|
|
2086
|
-
croppedCanvas.height = cropHeight;
|
|
2388
|
+
// Use pooled canvas for cropped version
|
|
2389
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2087
2390
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2391
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2088
2392
|
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2089
2393
|
const captureState = this.stateManager.getCaptureState();
|
|
2090
2394
|
if (captureState.step === 'front') {
|
|
@@ -2096,11 +2400,15 @@ const JaakStamps = class {
|
|
|
2096
2400
|
});
|
|
2097
2401
|
// Check if document classification is enabled
|
|
2098
2402
|
if (this.useDocumentClassification) {
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2403
|
+
try {
|
|
2404
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2405
|
+
if (classification && classification.class === 'passport') {
|
|
2406
|
+
this.completeProcess(true);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
catch (classifyError) {
|
|
2411
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2104
2412
|
}
|
|
2105
2413
|
}
|
|
2106
2414
|
this.stateManager.updateCaptureState({
|
|
@@ -2125,6 +2433,9 @@ const JaakStamps = class {
|
|
|
2125
2433
|
});
|
|
2126
2434
|
this.completeProcess(false);
|
|
2127
2435
|
}
|
|
2436
|
+
// Return canvases to pool after use
|
|
2437
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2438
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2128
2439
|
}
|
|
2129
2440
|
triggerCaptureAnimation() {
|
|
2130
2441
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -2134,6 +2445,135 @@ const JaakStamps = class {
|
|
|
2134
2445
|
cardOutline?.classList.remove('capturing');
|
|
2135
2446
|
}, 600);
|
|
2136
2447
|
}
|
|
2448
|
+
// Método simplificado para cambio a modo manual
|
|
2449
|
+
switchToManualMode() {
|
|
2450
|
+
// Only switch if detector is enabled and not already switched
|
|
2451
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
// Detener todo el monitoreo de performance
|
|
2455
|
+
this.stopPerformanceMonitoring();
|
|
2456
|
+
// Liberar recursos de ONNX
|
|
2457
|
+
this.releaseOnnxResources();
|
|
2458
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2459
|
+
this.resetMaskToWhite();
|
|
2460
|
+
this.detectionBoxes = [];
|
|
2461
|
+
this.hasAutoSwitchedToManual = true;
|
|
2462
|
+
this.performanceDegradedMode = true;
|
|
2463
|
+
this.showManualCaptureButton = true;
|
|
2464
|
+
this.showPerformanceMessage = true;
|
|
2465
|
+
// Hide performance message after 5 seconds
|
|
2466
|
+
setTimeout(() => {
|
|
2467
|
+
this.showPerformanceMessage = false;
|
|
2468
|
+
}, 5000);
|
|
2469
|
+
}
|
|
2470
|
+
// Método para cambiar a modo manual por errores de ONNX
|
|
2471
|
+
switchToManualModeWithWarning(warningMessage) {
|
|
2472
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2473
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
// No necesitamos liberar recursos ONNX porque no se han cargado aún
|
|
2477
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2478
|
+
this.resetMaskToWhite();
|
|
2479
|
+
this.detectionBoxes = [];
|
|
2480
|
+
// Cambiar a modo manual
|
|
2481
|
+
this.hasAutoSwitchedToManual = true;
|
|
2482
|
+
this.performanceDegradedMode = true;
|
|
2483
|
+
this.showManualCaptureButton = true;
|
|
2484
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2485
|
+
// Actualizar el estado con mensaje informativo (no error)
|
|
2486
|
+
this.updateStatus('Modo manual activado', warningMessage, 'active');
|
|
2487
|
+
// Continuar con la configuración de cámara
|
|
2488
|
+
this.continueWithCameraSetup();
|
|
2489
|
+
}
|
|
2490
|
+
async continueWithCameraSetup() {
|
|
2491
|
+
try {
|
|
2492
|
+
// Paso 2: Detectar y configurar dispositivos
|
|
2493
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
2494
|
+
try {
|
|
2495
|
+
await this.cameraService.enumerateDevices();
|
|
2496
|
+
}
|
|
2497
|
+
catch (enumerateError) {
|
|
2498
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
2499
|
+
}
|
|
2500
|
+
try {
|
|
2501
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2502
|
+
}
|
|
2503
|
+
catch (cameraInfoError) {
|
|
2504
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
2505
|
+
}
|
|
2506
|
+
// Paso 3: Configurar cámara seleccionada
|
|
2507
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
2508
|
+
let stream;
|
|
2509
|
+
try {
|
|
2510
|
+
stream = await this.cameraService.setupCamera();
|
|
2511
|
+
}
|
|
2512
|
+
catch (setupError) {
|
|
2513
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
2514
|
+
}
|
|
2515
|
+
// Paso 4: Inicializar video
|
|
2516
|
+
this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
|
|
2517
|
+
try {
|
|
2518
|
+
await this.initializeVideoStream(stream);
|
|
2519
|
+
}
|
|
2520
|
+
catch (videoError) {
|
|
2521
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
2522
|
+
}
|
|
2523
|
+
// Paso 5: Calibrar máscara
|
|
2524
|
+
if (this.detectionContainer) {
|
|
2525
|
+
const container = this.detectionContainer.parentElement;
|
|
2526
|
+
const rect = container.getBoundingClientRect();
|
|
2527
|
+
this.updateMaskDimensions(rect);
|
|
2528
|
+
}
|
|
2529
|
+
// Finalizar
|
|
2530
|
+
this.startTime = Date.now();
|
|
2531
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
2532
|
+
// Mostrar botón manual (ya debe estar habilitado)
|
|
2533
|
+
this.showManualCaptureButton = true;
|
|
2534
|
+
}
|
|
2535
|
+
catch (error) {
|
|
2536
|
+
this.updateStatus('Error de cámara', error.message, 'error');
|
|
2537
|
+
throw error;
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
switchToManualModeWithError(errorMessage) {
|
|
2541
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2542
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
// Liberar recursos de ONNX inmediatamente
|
|
2546
|
+
this.releaseOnnxResources();
|
|
2547
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2548
|
+
this.resetMaskToWhite();
|
|
2549
|
+
this.detectionBoxes = [];
|
|
2550
|
+
// Cambiar a modo manual
|
|
2551
|
+
this.hasAutoSwitchedToManual = true;
|
|
2552
|
+
this.performanceDegradedMode = true;
|
|
2553
|
+
this.showManualCaptureButton = true;
|
|
2554
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2555
|
+
// Actualizar el estado con mensaje de error
|
|
2556
|
+
this.updateStatus('Modo manual activado', errorMessage, 'error');
|
|
2557
|
+
// Ocultar mensaje después de 5 segundos
|
|
2558
|
+
setTimeout(() => {
|
|
2559
|
+
this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
|
|
2560
|
+
}, 5000);
|
|
2561
|
+
}
|
|
2562
|
+
stopPerformanceMonitoring() {
|
|
2563
|
+
// Nota: No necesitamos limpiar más variables porque ya no se usarán
|
|
2564
|
+
// El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
|
|
2565
|
+
}
|
|
2566
|
+
// Método para liberar recursos de ONNX cuando se cambia a modo manual
|
|
2567
|
+
releaseOnnxResources() {
|
|
2568
|
+
try {
|
|
2569
|
+
// Liberar recursos del servicio de detección
|
|
2570
|
+
if (this.detectionService) {
|
|
2571
|
+
this.detectionService.cleanup();
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
catch (error) {
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2137
2577
|
completeProcess(skippedBack = false) {
|
|
2138
2578
|
this.stateManager.updateCaptureState({
|
|
2139
2579
|
step: 'completed',
|
|
@@ -2153,11 +2593,6 @@ const JaakStamps = class {
|
|
|
2153
2593
|
setTimeout(() => {
|
|
2154
2594
|
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
2155
2595
|
}, 3000);
|
|
2156
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
2157
|
-
skippedBack,
|
|
2158
|
-
totalImages: capturedImages.metadata.totalImages,
|
|
2159
|
-
timestamp: new Date().toISOString()
|
|
2160
|
-
});
|
|
2161
2596
|
}
|
|
2162
2597
|
stopDetection() {
|
|
2163
2598
|
if (this.animationId) {
|
|
@@ -2166,7 +2601,6 @@ const JaakStamps = class {
|
|
|
2166
2601
|
}
|
|
2167
2602
|
this.clearBackDocumentTimer();
|
|
2168
2603
|
this.detectionBoxes = [];
|
|
2169
|
-
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
2170
2604
|
}
|
|
2171
2605
|
startBackDocumentTimer() {
|
|
2172
2606
|
if (!this.enableBackDocumentTimer)
|
|
@@ -2195,6 +2629,15 @@ const JaakStamps = class {
|
|
|
2195
2629
|
return; // Don't toggle if switching camera
|
|
2196
2630
|
this.showCameraSelector = !this.showCameraSelector;
|
|
2197
2631
|
}
|
|
2632
|
+
async updateCameraInfoWithAutofocus() {
|
|
2633
|
+
try {
|
|
2634
|
+
const cameraInfo = await this.cameraService.getCameraInfo();
|
|
2635
|
+
this.cameraInfoWithAutofocus = cameraInfo;
|
|
2636
|
+
}
|
|
2637
|
+
catch (error) {
|
|
2638
|
+
// Keep existing state if update fails
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2198
2641
|
async handleCameraSwitch(cameraId) {
|
|
2199
2642
|
if (this.isSwitchingCamera)
|
|
2200
2643
|
return; // Prevent multiple simultaneous switches
|
|
@@ -2202,10 +2645,6 @@ const JaakStamps = class {
|
|
|
2202
2645
|
// Close the selector immediately when user selects a camera
|
|
2203
2646
|
this.showCameraSelector = false;
|
|
2204
2647
|
this.isSwitchingCamera = true;
|
|
2205
|
-
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2206
|
-
from: this.cameraService.getSelectedCameraId(),
|
|
2207
|
-
to: cameraId
|
|
2208
|
-
});
|
|
2209
2648
|
// Stop current video stream
|
|
2210
2649
|
if (this.videoStream) {
|
|
2211
2650
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
@@ -2216,21 +2655,17 @@ const JaakStamps = class {
|
|
|
2216
2655
|
const newStream = await this.cameraService.setupCamera();
|
|
2217
2656
|
// Update video element
|
|
2218
2657
|
await this.initializeVideoStream(newStream);
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2222
|
-
});
|
|
2658
|
+
// Update camera info with autofocus data
|
|
2659
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2223
2660
|
}
|
|
2224
2661
|
catch (error) {
|
|
2225
|
-
this.logger.error('Error al cambiar cámara:', error);
|
|
2226
2662
|
// Try to restore previous camera if switch failed
|
|
2227
2663
|
try {
|
|
2228
2664
|
const fallbackStream = await this.cameraService.setupCamera();
|
|
2229
2665
|
await this.initializeVideoStream(fallbackStream);
|
|
2230
2666
|
}
|
|
2231
2667
|
catch (fallbackError) {
|
|
2232
|
-
this.
|
|
2233
|
-
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2668
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
|
|
2234
2669
|
}
|
|
2235
2670
|
}
|
|
2236
2671
|
finally {
|
|
@@ -2252,6 +2687,13 @@ const JaakStamps = class {
|
|
|
2252
2687
|
this.detectionBoxes = [];
|
|
2253
2688
|
this.alignmentStartTime = undefined;
|
|
2254
2689
|
this.hasDocumentDetected = false;
|
|
2690
|
+
// Reset sistema simplificado
|
|
2691
|
+
this.processedFramesCount = 0;
|
|
2692
|
+
this.slowFrameCount = 0;
|
|
2693
|
+
// Reset performance degradation state
|
|
2694
|
+
this.hasAutoSwitchedToManual = false;
|
|
2695
|
+
this.performanceDegradedMode = false;
|
|
2696
|
+
this.showPerformanceMessage = false;
|
|
2255
2697
|
if (this.alignmentTimer) {
|
|
2256
2698
|
clearTimeout(this.alignmentTimer);
|
|
2257
2699
|
this.alignmentTimer = undefined;
|
|
@@ -2271,7 +2713,7 @@ const JaakStamps = class {
|
|
|
2271
2713
|
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
2272
2714
|
}
|
|
2273
2715
|
this.isMaskReady = false;
|
|
2274
|
-
this.updateStatus('
|
|
2716
|
+
this.updateStatus('Captura finalizada', '', 'ready');
|
|
2275
2717
|
this.detectionBoxes = [];
|
|
2276
2718
|
this.cleanup();
|
|
2277
2719
|
}
|
|
@@ -2282,7 +2724,6 @@ const JaakStamps = class {
|
|
|
2282
2724
|
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2283
2725
|
this.updatePerformanceMetrics();
|
|
2284
2726
|
}, 500);
|
|
2285
|
-
this.logger.debug('Monitor de performance inicializado');
|
|
2286
2727
|
}
|
|
2287
2728
|
updatePerformanceMetrics() {
|
|
2288
2729
|
const currentTime = performance.now();
|
|
@@ -2325,6 +2766,84 @@ const JaakStamps = class {
|
|
|
2325
2766
|
if (detectionsFound > 0) {
|
|
2326
2767
|
this.performanceMetrics.successfulDetections++;
|
|
2327
2768
|
}
|
|
2769
|
+
// Update performance history for adaptive frame skipping
|
|
2770
|
+
this.performanceHistory.push(processingTime);
|
|
2771
|
+
if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
|
|
2772
|
+
this.performanceHistory.shift();
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
// Adaptive frame skipping based on performance
|
|
2776
|
+
getAdaptiveFrameSkip() {
|
|
2777
|
+
// Base conditions for high frame skip
|
|
2778
|
+
if (this.consecutiveFailures > 20)
|
|
2779
|
+
return this.MAX_FRAME_SKIP;
|
|
2780
|
+
if (this.performanceMetrics.inferenceTime > 200)
|
|
2781
|
+
return 6;
|
|
2782
|
+
if (this.performanceMetrics.memoryUsage > 200)
|
|
2783
|
+
return 5;
|
|
2784
|
+
// Calculate average processing time
|
|
2785
|
+
if (this.performanceHistory.length > 0) {
|
|
2786
|
+
const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
|
|
2787
|
+
if (avgProcessingTime > 100)
|
|
2788
|
+
return 4;
|
|
2789
|
+
if (avgProcessingTime > 80)
|
|
2790
|
+
return 3;
|
|
2791
|
+
if (avgProcessingTime > 60)
|
|
2792
|
+
return this.BASE_FRAME_SKIP + 1;
|
|
2793
|
+
}
|
|
2794
|
+
// Low memory devices get higher frame skip
|
|
2795
|
+
if (this.performanceMetrics.memoryUsage > 150)
|
|
2796
|
+
return 4;
|
|
2797
|
+
// Performance is good, use base frame skip
|
|
2798
|
+
return this.BASE_FRAME_SKIP;
|
|
2799
|
+
}
|
|
2800
|
+
// Método para resetear la máscara a color blanco
|
|
2801
|
+
resetMaskToWhite() {
|
|
2802
|
+
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
2803
|
+
const corners = this.el.shadowRoot?.querySelectorAll('.corner');
|
|
2804
|
+
const topSide = this.el.shadowRoot?.querySelector('.side-top');
|
|
2805
|
+
const rightSide = this.el.shadowRoot?.querySelector('.side-right');
|
|
2806
|
+
const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
|
|
2807
|
+
const leftSide = this.el.shadowRoot?.querySelector('.side-left');
|
|
2808
|
+
// Remover todas las clases de alineación y estado perfecto
|
|
2809
|
+
cardOutline?.classList.remove('perfect-match');
|
|
2810
|
+
corners?.forEach(corner => corner.classList.remove('perfect-match'));
|
|
2811
|
+
topSide?.classList.remove('aligned');
|
|
2812
|
+
rightSide?.classList.remove('aligned');
|
|
2813
|
+
bottomSide?.classList.remove('aligned');
|
|
2814
|
+
leftSide?.classList.remove('aligned');
|
|
2815
|
+
// Resetear estado de alineación
|
|
2816
|
+
this.sideAlignment = { top: false, right: false, bottom: false, left: false };
|
|
2817
|
+
}
|
|
2818
|
+
// Canvas pool management for screenshots
|
|
2819
|
+
getPooledCanvas(width, height) {
|
|
2820
|
+
// Try to find a canvas with matching dimensions
|
|
2821
|
+
const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
|
|
2822
|
+
if (matchingIndex !== -1) {
|
|
2823
|
+
return this.canvasPool.splice(matchingIndex, 1)[0];
|
|
2824
|
+
}
|
|
2825
|
+
// If no matching canvas, try to reuse any canvas and resize
|
|
2826
|
+
if (this.canvasPool.length > 0) {
|
|
2827
|
+
const canvas = this.canvasPool.pop();
|
|
2828
|
+
canvas.width = width;
|
|
2829
|
+
canvas.height = height;
|
|
2830
|
+
return canvas;
|
|
2831
|
+
}
|
|
2832
|
+
// Create new canvas if pool is empty
|
|
2833
|
+
const canvas = document.createElement('canvas');
|
|
2834
|
+
canvas.width = width;
|
|
2835
|
+
canvas.height = height;
|
|
2836
|
+
return canvas;
|
|
2837
|
+
}
|
|
2838
|
+
returnCanvasToPool(canvas) {
|
|
2839
|
+
// Only return to pool if not at max capacity
|
|
2840
|
+
if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
|
|
2841
|
+
// Clear the canvas before returning to pool
|
|
2842
|
+
const ctx = canvas.getContext('2d');
|
|
2843
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2844
|
+
this.canvasPool.push(canvas);
|
|
2845
|
+
}
|
|
2846
|
+
// If pool is full, let the canvas be garbage collected
|
|
2328
2847
|
}
|
|
2329
2848
|
};
|
|
2330
2849
|
JaakStamps.style = myComponentCss;
|