@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,45 +1,5 @@
|
|
|
1
1
|
import { p as proxyCustomElement, H, c as createEvent, h } from './index.js';
|
|
2
2
|
|
|
3
|
-
class LoggerService {
|
|
4
|
-
debugMode;
|
|
5
|
-
constructor(debug = false) {
|
|
6
|
-
this.debugMode = debug;
|
|
7
|
-
}
|
|
8
|
-
setDebugMode(debug) {
|
|
9
|
-
this.debugMode = debug;
|
|
10
|
-
}
|
|
11
|
-
info(...args) {
|
|
12
|
-
if (this.debugMode) {
|
|
13
|
-
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
warn(...args) {
|
|
17
|
-
if (this.debugMode) {
|
|
18
|
-
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
error(...args) {
|
|
22
|
-
if (this.debugMode) {
|
|
23
|
-
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
debug(...args) {
|
|
27
|
-
if (this.debugMode) {
|
|
28
|
-
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
state(state, data) {
|
|
32
|
-
if (this.debugMode) {
|
|
33
|
-
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
performance(operation, duration) {
|
|
37
|
-
if (this.debugMode) {
|
|
38
|
-
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
3
|
class EventBusService {
|
|
44
4
|
events = new Map();
|
|
45
5
|
on(event, callback) {
|
|
@@ -65,7 +25,7 @@ class EventBusService {
|
|
|
65
25
|
callback(data);
|
|
66
26
|
}
|
|
67
27
|
catch (error) {
|
|
68
|
-
|
|
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$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H {
|
|
1302
1123
|
constructor() {
|
|
@@ -1312,6 +1133,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1312
1133
|
maskSize = 90;
|
|
1313
1134
|
cropMargin = 20;
|
|
1314
1135
|
useDocumentClassification = false;
|
|
1136
|
+
useDocumentDetector = true;
|
|
1315
1137
|
preferredCamera = 'auto';
|
|
1316
1138
|
captureDelay = 1500;
|
|
1317
1139
|
enableBackDocumentTimer = false;
|
|
@@ -1328,9 +1150,16 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1328
1150
|
showCameraSelector = false;
|
|
1329
1151
|
isSwitchingCamera = false;
|
|
1330
1152
|
hasDocumentDetected = false;
|
|
1153
|
+
cameraInfoWithAutofocus = {
|
|
1154
|
+
availableCameras: [],
|
|
1155
|
+
selectedCameraId: null,
|
|
1156
|
+
deviceType: 'desktop',
|
|
1157
|
+
isMultipleCamerasAvailable: false,
|
|
1158
|
+
preferredFacing: null
|
|
1159
|
+
};
|
|
1331
1160
|
currentStatus = {
|
|
1332
|
-
message: '
|
|
1333
|
-
description: 'Configurando
|
|
1161
|
+
message: 'Preparando capturador...',
|
|
1162
|
+
description: 'Configurando herramientas de captura',
|
|
1334
1163
|
type: 'initializing',
|
|
1335
1164
|
isInitialized: false
|
|
1336
1165
|
};
|
|
@@ -1345,9 +1174,13 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1345
1174
|
detectionRate: 0
|
|
1346
1175
|
};
|
|
1347
1176
|
backDocumentTimerRemaining = 0;
|
|
1177
|
+
showManualCaptureButton = false;
|
|
1178
|
+
performanceDegradedMode = false; // Auto-switched to manual mode due to performance
|
|
1179
|
+
showPerformanceMessage = false; // Show performance message temporarily
|
|
1180
|
+
captureStateVersion = 0; // Triggers re-render when StateManager changes
|
|
1181
|
+
processingButton = null; // Track which button is processing
|
|
1348
1182
|
// Services
|
|
1349
1183
|
serviceContainer;
|
|
1350
|
-
logger;
|
|
1351
1184
|
eventBus;
|
|
1352
1185
|
stateManager;
|
|
1353
1186
|
cameraService;
|
|
@@ -1364,12 +1197,12 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1364
1197
|
alignmentStartTime;
|
|
1365
1198
|
alignmentTimer;
|
|
1366
1199
|
backDocumentTimer;
|
|
1200
|
+
orientationTimer;
|
|
1367
1201
|
// Performance monitoring
|
|
1368
1202
|
performanceMetrics = {
|
|
1369
1203
|
fps: 0,
|
|
1370
1204
|
inferenceTime: 0,
|
|
1371
1205
|
memoryUsage: 0,
|
|
1372
|
-
cpuUsage: 0,
|
|
1373
1206
|
onnxLoadTime: 0,
|
|
1374
1207
|
frameProcessingTime: 0,
|
|
1375
1208
|
totalDetections: 0,
|
|
@@ -1378,22 +1211,39 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1378
1211
|
lastUpdateTime: 0
|
|
1379
1212
|
};
|
|
1380
1213
|
performanceUpdateInterval;
|
|
1381
|
-
// Performance optimization
|
|
1214
|
+
// Performance optimization with adaptive frame skipping
|
|
1382
1215
|
frameSkipCounter = 0;
|
|
1383
|
-
|
|
1216
|
+
BASE_FRAME_SKIP = 2;
|
|
1217
|
+
MAX_FRAME_SKIP = 8;
|
|
1384
1218
|
consecutiveFailures = 0;
|
|
1385
1219
|
MAX_FAILURES = 30;
|
|
1386
1220
|
lastInferenceTime = 0;
|
|
1387
1221
|
MIN_INFERENCE_INTERVAL = 50;
|
|
1222
|
+
performanceHistory = [];
|
|
1223
|
+
PERFORMANCE_HISTORY_SIZE = 10;
|
|
1224
|
+
// Sistema simplificado de monitoreo de rendimiento
|
|
1225
|
+
PERFORMANCE_THRESHOLD_MS = 2000; // Umbral único para todos los frames
|
|
1226
|
+
SLOW_FRAMES_TO_TRIGGER = 2; // Cuántos frames lentos consecutivos antes de cambiar
|
|
1227
|
+
slowFrameCount = 0; // Contador de frames consecutivos lentos
|
|
1228
|
+
processedFramesCount = 0; // Contador total de frames procesados
|
|
1229
|
+
hasAutoSwitchedToManual = false;
|
|
1230
|
+
// Canvas pool for optimized screenshot capture
|
|
1231
|
+
canvasPool = [];
|
|
1232
|
+
MAX_CANVAS_POOL_SIZE = 3;
|
|
1388
1233
|
async componentDidLoad() {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
this.
|
|
1234
|
+
try {
|
|
1235
|
+
this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
|
|
1236
|
+
await this.initializeServices();
|
|
1237
|
+
this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
|
|
1238
|
+
await this.setupEventListeners();
|
|
1239
|
+
this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1240
|
+
await this.initializeComponent();
|
|
1241
|
+
if (this.debug) {
|
|
1242
|
+
this.initializePerformanceMonitor();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (error) {
|
|
1246
|
+
this.updateStatus('Error de inicialización', error.message, 'error');
|
|
1397
1247
|
}
|
|
1398
1248
|
}
|
|
1399
1249
|
async initializeServices() {
|
|
@@ -1403,37 +1253,22 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1403
1253
|
maskSize: this.maskSize,
|
|
1404
1254
|
cropMargin: this.cropMargin,
|
|
1405
1255
|
useDocumentClassification: this.useDocumentClassification,
|
|
1256
|
+
useDocumentDetector: this.useDocumentDetector,
|
|
1406
1257
|
preferredCamera: this.preferredCamera,
|
|
1407
1258
|
captureDelay: this.captureDelay
|
|
1408
1259
|
};
|
|
1409
1260
|
this.serviceContainer = new ServiceContainer(config);
|
|
1410
|
-
this.logger = this.serviceContainer.getLogger();
|
|
1411
1261
|
this.eventBus = this.serviceContainer.getEventBus();
|
|
1412
1262
|
this.stateManager = this.serviceContainer.getStateManager();
|
|
1413
1263
|
this.cameraService = this.serviceContainer.getCameraService();
|
|
1414
1264
|
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1415
|
-
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1416
1265
|
}
|
|
1417
1266
|
async setupEventListeners() {
|
|
1418
|
-
this.eventBus.on('state-changed', (
|
|
1419
|
-
this.handleStateChange(
|
|
1420
|
-
});
|
|
1421
|
-
this.eventBus.on('camera-changed', (cameraId) => {
|
|
1422
|
-
this.logger.state('CAMARA_CAMBIADA_EVENT', { cameraId });
|
|
1423
|
-
});
|
|
1424
|
-
this.eventBus.on('error', (error) => {
|
|
1425
|
-
this.logger.error('Error en servicio:', error);
|
|
1267
|
+
this.eventBus.on('state-changed', () => {
|
|
1268
|
+
this.handleStateChange();
|
|
1426
1269
|
});
|
|
1427
1270
|
}
|
|
1428
1271
|
async initializeComponent() {
|
|
1429
|
-
this.logger.state('COMPONENTE_INICIALIZANDO', {
|
|
1430
|
-
debug: this.debug,
|
|
1431
|
-
maskSize: this.maskSize,
|
|
1432
|
-
cropMargin: this.cropMargin,
|
|
1433
|
-
useDocumentClassification: this.useDocumentClassification,
|
|
1434
|
-
preferredCamera: this.preferredCamera,
|
|
1435
|
-
captureDelay: this.captureDelay
|
|
1436
|
-
});
|
|
1437
1272
|
this.validateProps();
|
|
1438
1273
|
if (this.debug) {
|
|
1439
1274
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
@@ -1442,58 +1277,67 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1442
1277
|
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1443
1278
|
await this.cameraService.detectDeviceType();
|
|
1444
1279
|
await this.cameraService.enumerateDevices();
|
|
1445
|
-
this.
|
|
1280
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1281
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
|
|
1446
1282
|
await this.loadOnnxRuntime();
|
|
1447
1283
|
this.initializeResizeObserver();
|
|
1448
1284
|
}
|
|
1449
1285
|
validateProps() {
|
|
1450
1286
|
if (this.maskSize < 50 || this.maskSize > 100) {
|
|
1451
|
-
this.logger.warn(`Propiedad maskSize inválida. Valor: ${this.maskSize}, esperado: 50-100. Usando valor por defecto: 90`);
|
|
1452
1287
|
this.maskSize = 90;
|
|
1453
1288
|
}
|
|
1454
1289
|
if (this.cropMargin < 0 || this.cropMargin > 100) {
|
|
1455
|
-
this.logger.warn(`Propiedad cropMargin inválida. Valor: ${this.cropMargin}, esperado: 0-100. Usando valor por defecto: 0`);
|
|
1456
1290
|
this.cropMargin = 0;
|
|
1457
1291
|
}
|
|
1458
1292
|
const validOptions = ['auto', 'front', 'back'];
|
|
1459
1293
|
if (!validOptions.includes(this.preferredCamera)) {
|
|
1460
|
-
this.logger.warn(`Propiedad preferredCamera inválida. Valor: ${this.preferredCamera}, esperado: ${validOptions.join(', ')}. Usando valor por defecto: 'auto'`);
|
|
1461
1294
|
this.preferredCamera = 'auto';
|
|
1462
1295
|
}
|
|
1463
1296
|
if (this.captureDelay < 0 || this.captureDelay > 10000) {
|
|
1464
|
-
this.logger.warn(`Propiedad captureDelay inválida. Valor: ${this.captureDelay}, esperado: 0-10000. Usando valor por defecto: 1500`);
|
|
1465
1297
|
this.captureDelay = 1500;
|
|
1466
1298
|
}
|
|
1467
1299
|
}
|
|
1468
1300
|
async loadOnnxRuntime() {
|
|
1469
1301
|
if (!window.ort) {
|
|
1470
|
-
this.updateStatus('
|
|
1302
|
+
this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
|
|
1471
1303
|
const script = document.createElement('script');
|
|
1472
1304
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
1473
1305
|
document.head.appendChild(script);
|
|
1474
1306
|
await new Promise((resolve) => {
|
|
1475
1307
|
script.onload = () => {
|
|
1476
|
-
setTimeout(() => {
|
|
1477
|
-
this.finalizeInitialization();
|
|
1308
|
+
setTimeout(async () => {
|
|
1309
|
+
await this.finalizeInitialization();
|
|
1478
1310
|
resolve(undefined);
|
|
1479
1311
|
}, 300);
|
|
1480
1312
|
};
|
|
1481
1313
|
});
|
|
1482
1314
|
}
|
|
1483
1315
|
else {
|
|
1484
|
-
setTimeout(() => {
|
|
1485
|
-
this.finalizeInitialization();
|
|
1316
|
+
setTimeout(async () => {
|
|
1317
|
+
await this.finalizeInitialization();
|
|
1486
1318
|
}, 300);
|
|
1487
1319
|
}
|
|
1488
1320
|
}
|
|
1489
|
-
finalizeInitialization() {
|
|
1321
|
+
async finalizeInitialization() {
|
|
1490
1322
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1323
|
+
// Check camera permissions before showing "ready" status
|
|
1324
|
+
const hasPermissions = await this.checkCameraPermissions();
|
|
1325
|
+
if (hasPermissions) {
|
|
1326
|
+
this.currentStatus = {
|
|
1327
|
+
message: 'Listo para capturar',
|
|
1328
|
+
description: '',
|
|
1329
|
+
type: 'ready',
|
|
1330
|
+
isInitialized: true
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
else {
|
|
1334
|
+
this.currentStatus = {
|
|
1335
|
+
message: 'Permisos de cámara requeridos',
|
|
1336
|
+
description: 'Por favor, otorgue permisos de cámara para continuar',
|
|
1337
|
+
type: 'error',
|
|
1338
|
+
isInitialized: true
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1497
1341
|
this.emitReadyEvent();
|
|
1498
1342
|
}
|
|
1499
1343
|
updateStatus(message, description, type = 'loading') {
|
|
@@ -1507,13 +1351,29 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1507
1351
|
emitReadyEvent() {
|
|
1508
1352
|
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1509
1353
|
this.isReady.emit(isDocumentReady);
|
|
1510
|
-
this.logger.state('COMPONENTE_LISTO', {
|
|
1511
|
-
ortLibraryLoaded: !!window.ort,
|
|
1512
|
-
modelPreloaded: this.detectionService.isModelLoaded(),
|
|
1513
|
-
isReady: isDocumentReady
|
|
1514
|
-
});
|
|
1515
1354
|
}
|
|
1516
|
-
|
|
1355
|
+
async checkCameraPermissions() {
|
|
1356
|
+
try {
|
|
1357
|
+
if (!navigator.permissions) {
|
|
1358
|
+
// Fallback: try to access camera directly
|
|
1359
|
+
try {
|
|
1360
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
1361
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
1369
|
+
return permission.state === 'granted';
|
|
1370
|
+
}
|
|
1371
|
+
catch (error) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
handleStateChange() {
|
|
1376
|
+
// Handle state changes from StateManager if needed
|
|
1517
1377
|
}
|
|
1518
1378
|
initializeResizeObserver() {
|
|
1519
1379
|
if ('ResizeObserver' in window && this.detectionContainer) {
|
|
@@ -1522,15 +1382,56 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1522
1382
|
});
|
|
1523
1383
|
resizeObserver.observe(this.detectionContainer.parentElement);
|
|
1524
1384
|
}
|
|
1385
|
+
// Handle mobile device orientation changes
|
|
1386
|
+
window.addEventListener('orientationchange', () => {
|
|
1387
|
+
// Clear any existing orientation timer
|
|
1388
|
+
if (this.orientationTimer) {
|
|
1389
|
+
clearTimeout(this.orientationTimer);
|
|
1390
|
+
}
|
|
1391
|
+
// Add delay to ensure video dimensions are updated after rotation
|
|
1392
|
+
this.orientationTimer = window.setTimeout(() => {
|
|
1393
|
+
this.handleResize();
|
|
1394
|
+
this.orientationTimer = undefined;
|
|
1395
|
+
}, 300);
|
|
1396
|
+
});
|
|
1525
1397
|
}
|
|
1526
1398
|
handleResize() {
|
|
1527
1399
|
if (this.detectionContainer) {
|
|
1528
1400
|
const container = this.detectionContainer.parentElement;
|
|
1529
1401
|
const rect = container.getBoundingClientRect();
|
|
1530
1402
|
this.updateMaskDimensions(rect);
|
|
1531
|
-
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
1532
1403
|
}
|
|
1533
1404
|
}
|
|
1405
|
+
triggerRerender() {
|
|
1406
|
+
this.captureStateVersion = this.captureStateVersion + 1;
|
|
1407
|
+
}
|
|
1408
|
+
getGuideText(step) {
|
|
1409
|
+
if (step === 'completed') {
|
|
1410
|
+
return 'Proceso completado';
|
|
1411
|
+
}
|
|
1412
|
+
// Check if we should show performance message
|
|
1413
|
+
if (this.showPerformanceMessage && step === 'front') {
|
|
1414
|
+
return 'Modo manual activado por rendimiento. Use el botón para capturar.';
|
|
1415
|
+
}
|
|
1416
|
+
if (step === 'front') {
|
|
1417
|
+
// Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
|
|
1418
|
+
if (this.showManualCaptureButton) {
|
|
1419
|
+
return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
|
|
1420
|
+
}
|
|
1421
|
+
// Instrucción básica de alineación
|
|
1422
|
+
return 'Alinee su identificación con el marco';
|
|
1423
|
+
}
|
|
1424
|
+
else if (step === 'back') {
|
|
1425
|
+
// Para el reverso, mostrar instrucciones específicas según el modo
|
|
1426
|
+
if (this.useDocumentDetector) {
|
|
1427
|
+
return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return 'Alinee su identificación con el marco';
|
|
1434
|
+
}
|
|
1534
1435
|
updateMaskDimensions(containerRect) {
|
|
1535
1436
|
if (!this.videoRef)
|
|
1536
1437
|
return;
|
|
@@ -1582,16 +1483,38 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1582
1483
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
1583
1484
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
1584
1485
|
this.isMaskReady = true;
|
|
1585
|
-
this.logger.state('DIMENSIONES_MASCARA_ACTUALIZADAS', {
|
|
1586
|
-
video: { width: videoWidth, height: videoHeight },
|
|
1587
|
-
displayed: { width: displayedVideoWidth, height: displayedVideoHeight },
|
|
1588
|
-
mask: { widthPercent: maskWidthPercent, heightPercent: maskHeightPercent },
|
|
1589
|
-
center: { x: videoCenterXPercent, y: videoCenterYPercent },
|
|
1590
|
-
offset: { x: videoOffsetX, y: videoOffsetY }
|
|
1591
|
-
});
|
|
1592
1486
|
}
|
|
1593
1487
|
// PUBLIC METHODS
|
|
1488
|
+
// Helper method to check if component is ready for operations
|
|
1489
|
+
isComponentReady() {
|
|
1490
|
+
if (!this.currentStatus.isInitialized) {
|
|
1491
|
+
return {
|
|
1492
|
+
ready: false,
|
|
1493
|
+
message: 'El componente aún se está inicializando',
|
|
1494
|
+
status: 'initializing'
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
|
|
1498
|
+
return {
|
|
1499
|
+
ready: false,
|
|
1500
|
+
message: 'El componente está cargando recursos',
|
|
1501
|
+
status: 'loading'
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
|
|
1505
|
+
return {
|
|
1506
|
+
ready: false,
|
|
1507
|
+
message: 'Los servicios del componente no están disponibles',
|
|
1508
|
+
status: 'error'
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
return { ready: true };
|
|
1512
|
+
}
|
|
1594
1513
|
async getCapturedImages() {
|
|
1514
|
+
const readyCheck = this.isComponentReady();
|
|
1515
|
+
if (!readyCheck.ready) {
|
|
1516
|
+
throw new Error(readyCheck.message);
|
|
1517
|
+
}
|
|
1595
1518
|
const state = this.stateManager.getCaptureState();
|
|
1596
1519
|
if (state.step !== 'completed') {
|
|
1597
1520
|
throw new Error('El proceso de captura no ha sido completado');
|
|
@@ -1599,146 +1522,359 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1599
1522
|
return this.stateManager.getCapturedImages();
|
|
1600
1523
|
}
|
|
1601
1524
|
async isProcessCompleted() {
|
|
1525
|
+
const readyCheck = this.isComponentReady();
|
|
1526
|
+
if (!readyCheck.ready) {
|
|
1527
|
+
return false; // Return false instead of error for this method
|
|
1528
|
+
}
|
|
1602
1529
|
return this.stateManager.isProcessCompleted();
|
|
1603
1530
|
}
|
|
1604
1531
|
async startCapture() {
|
|
1605
|
-
|
|
1532
|
+
const readyCheck = this.isComponentReady();
|
|
1533
|
+
if (!readyCheck.ready) {
|
|
1534
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1535
|
+
return { success: false, error: readyCheck.message };
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
await this.startDetection();
|
|
1539
|
+
return { success: true };
|
|
1540
|
+
}
|
|
1541
|
+
catch (error) {
|
|
1542
|
+
this.updateStatus('Error al iniciar captura', error.message, 'error');
|
|
1543
|
+
return { success: false, error: error.message };
|
|
1544
|
+
}
|
|
1606
1545
|
}
|
|
1607
1546
|
async stopCapture() {
|
|
1608
|
-
this.
|
|
1547
|
+
const readyCheck = this.isComponentReady();
|
|
1548
|
+
if (!readyCheck.ready) {
|
|
1549
|
+
return { success: false, error: readyCheck.message };
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
this.exitSession();
|
|
1553
|
+
return { success: true };
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
return { success: false, error: error.message };
|
|
1557
|
+
}
|
|
1609
1558
|
}
|
|
1610
1559
|
async resetCapture() {
|
|
1611
|
-
this.
|
|
1560
|
+
const readyCheck = this.isComponentReady();
|
|
1561
|
+
if (!readyCheck.ready) {
|
|
1562
|
+
return { success: false, error: readyCheck.message };
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
this.resetDetection();
|
|
1566
|
+
return { success: true };
|
|
1567
|
+
}
|
|
1568
|
+
catch (error) {
|
|
1569
|
+
return { success: false, error: error.message };
|
|
1570
|
+
}
|
|
1612
1571
|
}
|
|
1613
1572
|
async skipBackCapture() {
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1573
|
+
console.log('🟡 Botón clicked: Saltar reverso');
|
|
1574
|
+
// Set processing state immediately
|
|
1575
|
+
this.processingButton = 'skip-back';
|
|
1576
|
+
// Add small delay to show spinner
|
|
1577
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1578
|
+
const readyCheck = this.isComponentReady();
|
|
1579
|
+
if (!readyCheck.ready) {
|
|
1580
|
+
this.processingButton = null;
|
|
1581
|
+
return { success: false, error: readyCheck.message };
|
|
1618
1582
|
}
|
|
1619
|
-
|
|
1583
|
+
try {
|
|
1584
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1585
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1586
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1587
|
+
this.completeProcess(true);
|
|
1588
|
+
return { success: true };
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
catch (error) {
|
|
1595
|
+
return { success: false, error: error.message };
|
|
1596
|
+
}
|
|
1597
|
+
finally {
|
|
1598
|
+
// Always clear processing state when done
|
|
1599
|
+
this.processingButton = null;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1620
1602
|
async getStatus() {
|
|
1621
|
-
const
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1603
|
+
const readyCheck = this.isComponentReady();
|
|
1604
|
+
if (!readyCheck.ready) {
|
|
1605
|
+
return {
|
|
1606
|
+
isVideoActive: false,
|
|
1607
|
+
captureStep: 'front',
|
|
1608
|
+
hasImages: false,
|
|
1609
|
+
isProcessCompleted: false,
|
|
1610
|
+
isModelPreloaded: false,
|
|
1611
|
+
componentStatus: readyCheck.status,
|
|
1612
|
+
componentMessage: readyCheck.message
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
try {
|
|
1616
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1617
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1618
|
+
return {
|
|
1619
|
+
isVideoActive: captureState.isVideoActive,
|
|
1620
|
+
captureStep: captureState.step,
|
|
1621
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1622
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1623
|
+
isModelPreloaded: this.detectionService.isModelLoaded(),
|
|
1624
|
+
componentStatus: 'ready'
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
return {
|
|
1629
|
+
isVideoActive: false,
|
|
1630
|
+
captureStep: 'front',
|
|
1631
|
+
hasImages: false,
|
|
1632
|
+
isProcessCompleted: false,
|
|
1633
|
+
isModelPreloaded: false,
|
|
1634
|
+
componentStatus: 'error',
|
|
1635
|
+
componentMessage: error.message
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1630
1638
|
}
|
|
1631
1639
|
async preloadModel() {
|
|
1640
|
+
const readyCheck = this.isComponentReady();
|
|
1641
|
+
if (!readyCheck.ready) {
|
|
1642
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1643
|
+
return { success: false, error: readyCheck.message };
|
|
1644
|
+
}
|
|
1632
1645
|
if (this.detectionService.isModelLoaded()) {
|
|
1633
|
-
this.
|
|
1634
|
-
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1646
|
+
this.updateStatus('Reconocimiento listo', '', 'ready');
|
|
1635
1647
|
return { success: true, message: 'Model already loaded' };
|
|
1636
1648
|
}
|
|
1637
1649
|
try {
|
|
1638
1650
|
const loadStartTime = performance.now();
|
|
1639
|
-
this.updateStatus('
|
|
1640
|
-
this.stateManager
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1651
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1652
|
+
if (this.stateManager) {
|
|
1653
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
await this.detectionService.loadModel();
|
|
1657
|
+
}
|
|
1658
|
+
catch (modelError) {
|
|
1659
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1660
|
+
throw modelError;
|
|
1661
|
+
}
|
|
1662
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1663
|
+
try {
|
|
1664
|
+
await this.detectionService.loadClassificationModel();
|
|
1665
|
+
}
|
|
1666
|
+
catch (classificationError) {
|
|
1667
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1668
|
+
throw classificationError;
|
|
1669
|
+
}
|
|
1644
1670
|
const loadEndTime = performance.now();
|
|
1645
1671
|
const loadTime = loadEndTime - loadStartTime;
|
|
1646
1672
|
// Record ONNX load time
|
|
1647
1673
|
if (this.debug) {
|
|
1648
1674
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1649
1675
|
}
|
|
1650
|
-
this.updateStatus('
|
|
1676
|
+
this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
|
|
1651
1677
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1652
|
-
this.updateStatus('
|
|
1653
|
-
this.stateManager
|
|
1678
|
+
this.updateStatus('Reconocimiento preparado', '', 'ready');
|
|
1679
|
+
if (this.stateManager) {
|
|
1680
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1681
|
+
}
|
|
1654
1682
|
this.emitReadyEvent();
|
|
1655
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1656
1683
|
return { success: true, message: 'Models preloaded successfully' };
|
|
1657
1684
|
}
|
|
1658
1685
|
catch (error) {
|
|
1659
|
-
this.
|
|
1660
|
-
this.
|
|
1661
|
-
|
|
1686
|
+
this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
|
|
1687
|
+
if (this.stateManager) {
|
|
1688
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1689
|
+
}
|
|
1662
1690
|
return { success: false, error: error.message };
|
|
1663
1691
|
}
|
|
1664
1692
|
}
|
|
1665
1693
|
async getCameraInfo() {
|
|
1666
|
-
|
|
1694
|
+
const readyCheck = this.isComponentReady();
|
|
1695
|
+
if (!readyCheck.ready) {
|
|
1696
|
+
return {
|
|
1697
|
+
availableCameras: [],
|
|
1698
|
+
selectedCameraId: null,
|
|
1699
|
+
deviceType: 'desktop',
|
|
1700
|
+
isMultipleCamerasAvailable: false,
|
|
1701
|
+
preferredFacing: null,
|
|
1702
|
+
error: readyCheck.message
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
try {
|
|
1706
|
+
return await this.cameraService.getCameraInfo();
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
return {
|
|
1710
|
+
availableCameras: [],
|
|
1711
|
+
selectedCameraId: null,
|
|
1712
|
+
deviceType: 'desktop',
|
|
1713
|
+
isMultipleCamerasAvailable: false,
|
|
1714
|
+
preferredFacing: null,
|
|
1715
|
+
error: error.message
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1667
1718
|
}
|
|
1668
1719
|
async setPreferredCamera(camera) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1720
|
+
const readyCheck = this.isComponentReady();
|
|
1721
|
+
if (!readyCheck.ready) {
|
|
1722
|
+
return {
|
|
1723
|
+
success: false,
|
|
1724
|
+
error: readyCheck.message,
|
|
1725
|
+
selectedCamera: null,
|
|
1726
|
+
availableCameras: 0
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
try {
|
|
1730
|
+
this.preferredCamera = camera;
|
|
1731
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1732
|
+
await this.cameraService.enumerateDevices();
|
|
1733
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1734
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1735
|
+
if (captureState.isVideoActive) {
|
|
1736
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1737
|
+
if (selectedCameraId) {
|
|
1738
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1739
|
+
}
|
|
1677
1740
|
}
|
|
1741
|
+
return {
|
|
1742
|
+
success: true,
|
|
1743
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1744
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
return {
|
|
1749
|
+
success: false,
|
|
1750
|
+
error: error.message,
|
|
1751
|
+
selectedCamera: null,
|
|
1752
|
+
availableCameras: 0
|
|
1753
|
+
};
|
|
1678
1754
|
}
|
|
1679
|
-
return {
|
|
1680
|
-
success: true,
|
|
1681
|
-
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1682
|
-
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1683
|
-
};
|
|
1684
1755
|
}
|
|
1685
1756
|
async setCaptureDelay(delay) {
|
|
1686
1757
|
if (delay < 0 || delay > 10000) {
|
|
1687
|
-
|
|
1758
|
+
return {
|
|
1759
|
+
success: false,
|
|
1760
|
+
error: 'Capture delay must be between 0 and 10000 milliseconds',
|
|
1761
|
+
captureDelay: this.captureDelay
|
|
1762
|
+
};
|
|
1688
1763
|
}
|
|
1764
|
+
// Allow setting capture delay even during initialization
|
|
1689
1765
|
this.captureDelay = delay;
|
|
1690
|
-
this.serviceContainer
|
|
1766
|
+
if (this.serviceContainer) {
|
|
1767
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1768
|
+
}
|
|
1691
1769
|
return {
|
|
1692
1770
|
success: true,
|
|
1693
1771
|
captureDelay: this.captureDelay
|
|
1694
1772
|
};
|
|
1695
1773
|
}
|
|
1696
1774
|
async getCaptureDelay() {
|
|
1775
|
+
// Always allow getting capture delay, even during initialization
|
|
1697
1776
|
return this.captureDelay;
|
|
1698
1777
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
};
|
|
1705
|
-
}
|
|
1706
|
-
async focusAtPoint(x, y) {
|
|
1707
|
-
const focused = await this.cameraService.focusAtPoint(x, y, this.videoStream);
|
|
1778
|
+
// Memory and device capability detection
|
|
1779
|
+
checkDeviceCapabilities() {
|
|
1780
|
+
const deviceMemory = navigator.deviceMemory;
|
|
1781
|
+
// Siempre intentar usar ML, independientemente del dispositivo
|
|
1782
|
+
// Las restricciones anteriores no eran confiables
|
|
1708
1783
|
return {
|
|
1709
|
-
|
|
1710
|
-
|
|
1784
|
+
canUseML: true,
|
|
1785
|
+
reason: 'Dispositivo compatible con detección automática.',
|
|
1786
|
+
memoryMB: deviceMemory ? deviceMemory * 1024 : null
|
|
1711
1787
|
};
|
|
1712
1788
|
}
|
|
1713
1789
|
// DETECTION METHODS
|
|
1714
1790
|
async startDetection() {
|
|
1715
|
-
this.logger.state('INICIANDO_DETECCION');
|
|
1716
1791
|
try {
|
|
1792
|
+
// Verificar capacidades del dispositivo antes de cargar modelos
|
|
1793
|
+
const capabilities = this.checkDeviceCapabilities();
|
|
1794
|
+
// Si el dispositivo no puede usar ML o tenemos problemas de memoria, usar modo manual
|
|
1795
|
+
if (!capabilities.canUseML && this.useDocumentDetector) {
|
|
1796
|
+
this.switchToManualModeWithWarning(capabilities.reason);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1717
1799
|
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1718
1800
|
if (!this.detectionService.isModelLoaded()) {
|
|
1719
1801
|
const loadStartTime = performance.now();
|
|
1720
|
-
this.updateStatus('
|
|
1802
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1721
1803
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1804
|
+
try {
|
|
1805
|
+
await this.detectionService.loadModel();
|
|
1806
|
+
}
|
|
1807
|
+
catch (modelError) {
|
|
1808
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1809
|
+
if (modelError.message.includes('Out of memory') || modelError.message.includes('no available backend')) {
|
|
1810
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1811
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para detección automática ${memoryInfo}. Modo manual activado - funciona igual de bien!`);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
this.switchToManualModeWithError('Error al cargar modelo de detección');
|
|
1816
|
+
throw modelError;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1820
|
+
try {
|
|
1821
|
+
await this.detectionService.loadClassificationModel();
|
|
1822
|
+
}
|
|
1823
|
+
catch (classificationError) {
|
|
1824
|
+
// Si es un error de memoria, cambiar a modo manual con mensaje específico
|
|
1825
|
+
if (classificationError.message.includes('Out of memory') || classificationError.message.includes('no available backend')) {
|
|
1826
|
+
const memoryInfo = navigator.deviceMemory ? `(${navigator.deviceMemory}GB disponible)` : '';
|
|
1827
|
+
this.switchToManualModeWithWarning(`Memoria insuficiente para clasificación automática ${memoryInfo}. Modo manual activado para optimizar rendimiento.`);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
else {
|
|
1831
|
+
this.switchToManualModeWithError('Error al cargar modelo de clasificación');
|
|
1832
|
+
throw classificationError;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1725
1835
|
const loadEndTime = performance.now();
|
|
1726
1836
|
const loadTime = loadEndTime - loadStartTime;
|
|
1727
1837
|
// Record ONNX load time
|
|
1728
1838
|
if (this.debug) {
|
|
1729
1839
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1730
1840
|
}
|
|
1731
|
-
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
1732
1841
|
}
|
|
1733
1842
|
// Paso 2: Detectar y configurar dispositivos
|
|
1734
|
-
this.updateStatus('
|
|
1735
|
-
|
|
1843
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
1844
|
+
try {
|
|
1845
|
+
await this.cameraService.enumerateDevices();
|
|
1846
|
+
}
|
|
1847
|
+
catch (enumerateError) {
|
|
1848
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1852
|
+
}
|
|
1853
|
+
catch (cameraInfoError) {
|
|
1854
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
1855
|
+
}
|
|
1736
1856
|
// Paso 3: Configurar cámara seleccionada
|
|
1737
|
-
this.updateStatus('
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1857
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
1858
|
+
let stream;
|
|
1859
|
+
try {
|
|
1860
|
+
stream = await this.cameraService.setupCamera();
|
|
1861
|
+
}
|
|
1862
|
+
catch (setupError) {
|
|
1863
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
1864
|
+
}
|
|
1865
|
+
// Paso 4: Inicializar video y mostrar estado de análisis inicial
|
|
1866
|
+
if (this.useDocumentDetector) {
|
|
1867
|
+
this.updateStatus('Analizando estabilidad...', 'Evaluando rendimiento del sistema', 'loading');
|
|
1868
|
+
}
|
|
1869
|
+
else {
|
|
1870
|
+
this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await this.initializeVideoStream(stream);
|
|
1874
|
+
}
|
|
1875
|
+
catch (videoError) {
|
|
1876
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
1877
|
+
}
|
|
1742
1878
|
// Paso 5: Calibrar máscara
|
|
1743
1879
|
if (this.detectionContainer) {
|
|
1744
1880
|
const container = this.detectionContainer.parentElement;
|
|
@@ -1748,11 +1884,14 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1748
1884
|
// Finalizar e iniciar captura
|
|
1749
1885
|
this.startTime = Date.now();
|
|
1750
1886
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1887
|
+
// Mostrar botón manual si el detector está deshabilitado
|
|
1888
|
+
if (!this.useDocumentDetector) {
|
|
1889
|
+
this.showManualCaptureButton = true;
|
|
1890
|
+
}
|
|
1751
1891
|
this.detectFrame();
|
|
1752
1892
|
}
|
|
1753
1893
|
catch (err) {
|
|
1754
|
-
this.
|
|
1755
|
-
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1894
|
+
this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
|
|
1756
1895
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1757
1896
|
}
|
|
1758
1897
|
}
|
|
@@ -1762,14 +1901,16 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1762
1901
|
this.videoStream = stream;
|
|
1763
1902
|
const isRear = this.cameraService.isRearCamera(stream);
|
|
1764
1903
|
this.shouldMirrorVideo = !isRear;
|
|
1765
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1766
|
-
isRearCamera: isRear,
|
|
1767
|
-
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1768
|
-
});
|
|
1769
1904
|
return new Promise((resolve) => {
|
|
1770
1905
|
this.videoRef.onloadedmetadata = async () => {
|
|
1771
1906
|
await this.videoRef.play();
|
|
1772
1907
|
this.stateManager.updateCaptureState({ isVideoActive: true });
|
|
1908
|
+
// Recalculate mask dimensions when new camera stream loads
|
|
1909
|
+
if (this.detectionContainer) {
|
|
1910
|
+
const container = this.detectionContainer.parentElement;
|
|
1911
|
+
const rect = container.getBoundingClientRect();
|
|
1912
|
+
this.updateMaskDimensions(rect);
|
|
1913
|
+
}
|
|
1773
1914
|
resolve();
|
|
1774
1915
|
};
|
|
1775
1916
|
});
|
|
@@ -1784,15 +1925,28 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1784
1925
|
const captureState = this.stateManager.getCaptureState();
|
|
1785
1926
|
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1786
1927
|
return;
|
|
1928
|
+
// Show manual capture button when document detector is disabled or performance is degraded
|
|
1929
|
+
if (!this.useDocumentDetector || this.performanceDegradedMode) {
|
|
1930
|
+
this.showManualCaptureButton = true;
|
|
1931
|
+
// Continue the loop for UI updates but skip detection logic
|
|
1932
|
+
if (captureState.step !== 'completed') {
|
|
1933
|
+
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
else if (!this.performanceDegradedMode) {
|
|
1938
|
+
this.showManualCaptureButton = false;
|
|
1939
|
+
}
|
|
1787
1940
|
if (captureState.isDetectionPaused) {
|
|
1788
1941
|
if (captureState.step !== 'completed') {
|
|
1789
1942
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1790
1943
|
}
|
|
1791
1944
|
return;
|
|
1792
1945
|
}
|
|
1793
|
-
//
|
|
1946
|
+
// Adaptive frame skipping for performance
|
|
1794
1947
|
this.frameSkipCounter++;
|
|
1795
|
-
|
|
1948
|
+
const currentFrameSkip = this.getAdaptiveFrameSkip();
|
|
1949
|
+
if (this.frameSkipCounter <= currentFrameSkip) {
|
|
1796
1950
|
if (captureState.step !== 'completed') {
|
|
1797
1951
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1798
1952
|
}
|
|
@@ -1810,10 +1964,27 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1810
1964
|
this.lastInferenceTime = currentTime;
|
|
1811
1965
|
// Measure preprocessing and inference time
|
|
1812
1966
|
const inferenceStartTime = performance.now();
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1967
|
+
let inputTensor;
|
|
1968
|
+
let detections;
|
|
1969
|
+
let inferenceTime;
|
|
1970
|
+
try {
|
|
1971
|
+
inputTensor = this.detectionService.preprocess(this.videoRef);
|
|
1972
|
+
}
|
|
1973
|
+
catch (preprocessError) {
|
|
1974
|
+
this.switchToManualModeWithError('Error al procesar imagen');
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
try {
|
|
1978
|
+
// Standard detection without timeout (timeout is handled by frame counting)
|
|
1979
|
+
detections = await this.detectionService.runInference(inputTensor);
|
|
1980
|
+
inferenceTime = performance.now() - inferenceStartTime;
|
|
1981
|
+
}
|
|
1982
|
+
catch (inferenceError) {
|
|
1983
|
+
this.switchToManualModeWithError('Error en detección automática');
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
// Increment frame count
|
|
1987
|
+
this.processedFramesCount++;
|
|
1817
1988
|
// Record ONNX performance metrics
|
|
1818
1989
|
if (this.debug) {
|
|
1819
1990
|
this.recordOnnxPerformance(0, inferenceTime);
|
|
@@ -1852,9 +2023,30 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1852
2023
|
}
|
|
1853
2024
|
this.updateMaskColor(detections);
|
|
1854
2025
|
// Record frame processing metrics
|
|
2026
|
+
const frameEndTime = performance.now();
|
|
2027
|
+
const totalFrameTime = frameEndTime - frameStartTime;
|
|
2028
|
+
// Sistema simplificado: Verificar rendimiento de cada frame (solo si no se ha cambiado a manual)
|
|
2029
|
+
if (this.useDocumentDetector && !this.hasAutoSwitchedToManual) {
|
|
2030
|
+
// Verificar si el frame es lento
|
|
2031
|
+
if (totalFrameTime >= this.PERFORMANCE_THRESHOLD_MS) {
|
|
2032
|
+
this.slowFrameCount++;
|
|
2033
|
+
// Si tenemos suficientes frames lentos consecutivos, cambiar a manual
|
|
2034
|
+
if (this.slowFrameCount >= this.SLOW_FRAMES_TO_TRIGGER) {
|
|
2035
|
+
this.switchToManualMode();
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
else {
|
|
2039
|
+
// Frame rápido, reiniciar contador
|
|
2040
|
+
if (this.slowFrameCount > 0) {
|
|
2041
|
+
this.slowFrameCount = 0;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
// Cambiar mensaje de estado después de analizar los primeros 2 frames
|
|
2045
|
+
if (this.processedFramesCount === 2) {
|
|
2046
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
1855
2049
|
if (this.debug) {
|
|
1856
|
-
const frameEndTime = performance.now();
|
|
1857
|
-
const totalFrameTime = frameEndTime - frameStartTime;
|
|
1858
2050
|
this.recordFrameProcessing(totalFrameTime, detections.length);
|
|
1859
2051
|
}
|
|
1860
2052
|
// Continue detection loop
|
|
@@ -1868,7 +2060,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1868
2060
|
}
|
|
1869
2061
|
}
|
|
1870
2062
|
catch (e) {
|
|
1871
|
-
this.logger.error('Error en inferencia de modelo:', e);
|
|
1872
2063
|
const captureState = this.stateManager.getCaptureState();
|
|
1873
2064
|
if (captureState.step !== 'completed') {
|
|
1874
2065
|
setTimeout(() => this.detectFrame(), 100);
|
|
@@ -1891,10 +2082,18 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1891
2082
|
clearTimeout(this.alignmentTimer);
|
|
1892
2083
|
this.alignmentTimer = undefined;
|
|
1893
2084
|
}
|
|
2085
|
+
if (this.orientationTimer) {
|
|
2086
|
+
clearTimeout(this.orientationTimer);
|
|
2087
|
+
this.orientationTimer = undefined;
|
|
2088
|
+
}
|
|
2089
|
+
// Liberar recursos de ONNX al limpiar el componente
|
|
2090
|
+
this.releaseOnnxResources();
|
|
1894
2091
|
if (this.performanceUpdateInterval) {
|
|
1895
2092
|
clearInterval(this.performanceUpdateInterval);
|
|
1896
2093
|
this.performanceUpdateInterval = undefined;
|
|
1897
2094
|
}
|
|
2095
|
+
// Clear canvas pool
|
|
2096
|
+
this.canvasPool = [];
|
|
1898
2097
|
this.detectionBoxes = [];
|
|
1899
2098
|
this.alignmentStartTime = undefined;
|
|
1900
2099
|
this.hasDocumentDetected = false;
|
|
@@ -1908,11 +2107,8 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1908
2107
|
step: 'front',
|
|
1909
2108
|
isCapturing: false
|
|
1910
2109
|
};
|
|
1911
|
-
const cameraInfo = this.
|
|
1912
|
-
|
|
1913
|
-
selectedCameraId: null,
|
|
1914
|
-
deviceType: 'desktop'};
|
|
1915
|
-
return (h("div", { key: '9d4d77042218ab3d0bc4fb1ce879db09dc3e78b4', class: "detector-container" }, h("div", { key: '51ac8ccb1275637282b038ad8364d19f14ef61b1', class: "video-container" }, h("video", { key: 'a34f0481da20ec5b7b62b21aa275ec5f6dfec5d1', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '43b71dfe3eaf20ffa66a4139e24b1af6a2074631', ref: el => this.detectionContainer = el, class: `detection-overlay ${this.shouldMirrorVideo ? 'mirror' : ''}` }, this.debug && this.detectionBoxes.map((box, index) => (h("div", { key: index, class: "detection-box", style: {
|
|
2110
|
+
const cameraInfo = this.cameraInfoWithAutofocus;
|
|
2111
|
+
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: {
|
|
1916
2112
|
position: 'absolute',
|
|
1917
2113
|
left: `${box.x}px`,
|
|
1918
2114
|
top: `${box.y}px`,
|
|
@@ -1921,9 +2117,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
1921
2117
|
border: '2px solid #32406C',
|
|
1922
2118
|
pointerEvents: 'none',
|
|
1923
2119
|
boxSizing: 'border-box'
|
|
1924
|
-
} })))), this.isMaskReady && (h("div", { key: '
|
|
2120
|
+
} })))), 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
|
|
1925
2121
|
? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
|
|
1926
|
-
: 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: '
|
|
2122
|
+
: '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" })))));
|
|
1927
2123
|
}
|
|
1928
2124
|
// Utility methods
|
|
1929
2125
|
updateDetectionBoxes(boxes) {
|
|
@@ -2004,35 +2200,18 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2004
2200
|
if (allSidesAligned && bestBox) {
|
|
2005
2201
|
cardOutline?.classList.add('perfect-match');
|
|
2006
2202
|
corners?.forEach(corner => corner.classList.add('perfect-match'));
|
|
2007
|
-
// Debug logging
|
|
2008
|
-
this.logger.state('CAPTURE_EVALUATION', {
|
|
2009
|
-
allSidesAligned: true,
|
|
2010
|
-
hasScreenshotTaken: this.hasScreenshotTaken,
|
|
2011
|
-
captureDelay: this.captureDelay,
|
|
2012
|
-
alignmentStartTime: this.alignmentStartTime
|
|
2013
|
-
});
|
|
2014
2203
|
if (!this.hasScreenshotTaken) {
|
|
2015
2204
|
const currentTime = Date.now();
|
|
2016
2205
|
// Initialize alignment start time if not set
|
|
2017
2206
|
if (!this.alignmentStartTime) {
|
|
2018
2207
|
this.alignmentStartTime = currentTime;
|
|
2019
|
-
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
2020
2208
|
}
|
|
2021
2209
|
// Check if document has been aligned for the configured delay
|
|
2022
2210
|
const alignmentDuration = currentTime - this.alignmentStartTime;
|
|
2023
|
-
this.logger.state('ALIGNMENT_DURATION_CHECK', {
|
|
2024
|
-
alignmentDuration,
|
|
2025
|
-
captureDelay: this.captureDelay,
|
|
2026
|
-
readyToCapture: alignmentDuration >= this.captureDelay
|
|
2027
|
-
});
|
|
2028
2211
|
if (alignmentDuration >= this.captureDelay) {
|
|
2029
|
-
this.logger.state('TRIGGERING_CAPTURE', {
|
|
2030
|
-
alignmentDuration,
|
|
2031
|
-
captureDelay: this.captureDelay
|
|
2032
|
-
});
|
|
2033
2212
|
this.lastDetectedBox = bestBox;
|
|
2034
|
-
this.takeScreenshot().catch(
|
|
2035
|
-
|
|
2213
|
+
this.takeScreenshot().catch(() => {
|
|
2214
|
+
// Handle screenshot error silently
|
|
2036
2215
|
});
|
|
2037
2216
|
this.hasScreenshotTaken = true;
|
|
2038
2217
|
this.alignmentStartTime = undefined;
|
|
@@ -2055,24 +2234,150 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2055
2234
|
}
|
|
2056
2235
|
}
|
|
2057
2236
|
}
|
|
2237
|
+
async takeManualScreenshot() {
|
|
2238
|
+
console.log('🔵 Botón clicked: Capturar (Frente/Reverso)');
|
|
2239
|
+
// Set processing state immediately
|
|
2240
|
+
const captureState = this.stateManager?.getCaptureState();
|
|
2241
|
+
if (captureState?.step === 'front') {
|
|
2242
|
+
this.processingButton = 'capture-front';
|
|
2243
|
+
}
|
|
2244
|
+
else if (captureState?.step === 'back') {
|
|
2245
|
+
this.processingButton = 'capture-back';
|
|
2246
|
+
}
|
|
2247
|
+
console.log('🔵 processingButton set to:', this.processingButton);
|
|
2248
|
+
// Add small delay to show spinner
|
|
2249
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2250
|
+
if (!this.videoRef) {
|
|
2251
|
+
this.processingButton = null;
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
// When using manual capture, use mask coordinates for cropping
|
|
2255
|
+
// Note: processingButton will be cleared inside takeScreenshotWithMaskCoordinates
|
|
2256
|
+
await this.takeScreenshotWithMaskCoordinates();
|
|
2257
|
+
}
|
|
2258
|
+
async takeScreenshotWithMaskCoordinates() {
|
|
2259
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
2260
|
+
return;
|
|
2261
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2262
|
+
this.triggerCaptureAnimation();
|
|
2263
|
+
// Use pooled canvas for optimization
|
|
2264
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2265
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2266
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2267
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2268
|
+
// Calculate mask coordinates for cropping
|
|
2269
|
+
const container = this.detectionContainer.parentElement;
|
|
2270
|
+
const containerRect = container.getBoundingClientRect();
|
|
2271
|
+
// Get mask dimensions from CSS properties
|
|
2272
|
+
const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
|
|
2273
|
+
const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
|
|
2274
|
+
const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
|
|
2275
|
+
const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
|
|
2276
|
+
// Convert percentages to actual video coordinates
|
|
2277
|
+
const videoWidth = this.videoRef.videoWidth;
|
|
2278
|
+
const videoHeight = this.videoRef.videoHeight;
|
|
2279
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
2280
|
+
const containerAspectRatio = containerRect.width / containerRect.height;
|
|
2281
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
2282
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
2283
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
2284
|
+
displayedVideoWidth = containerRect.width;
|
|
2285
|
+
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
2286
|
+
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
2287
|
+
}
|
|
2288
|
+
else {
|
|
2289
|
+
displayedVideoHeight = containerRect.height;
|
|
2290
|
+
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
2291
|
+
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
2292
|
+
}
|
|
2293
|
+
// Calculate mask coordinates in video space
|
|
2294
|
+
const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
|
|
2295
|
+
const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
|
|
2296
|
+
const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
|
|
2297
|
+
const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
|
|
2298
|
+
// Convert to video coordinates
|
|
2299
|
+
const scaleX = videoWidth / displayedVideoWidth;
|
|
2300
|
+
const scaleY = videoHeight / displayedVideoHeight;
|
|
2301
|
+
const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
|
|
2302
|
+
const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
|
|
2303
|
+
const maskWidthInVideo = maskWidthInContainer * scaleX;
|
|
2304
|
+
const maskHeightInVideo = maskHeightInContainer * scaleY;
|
|
2305
|
+
// Calculate crop coordinates
|
|
2306
|
+
const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
|
|
2307
|
+
const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
|
|
2308
|
+
const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
|
|
2309
|
+
const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
|
|
2310
|
+
// Use pooled canvas for cropped version
|
|
2311
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2312
|
+
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2313
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2314
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2315
|
+
const captureState = this.stateManager.getCaptureState();
|
|
2316
|
+
if (captureState.step === 'front') {
|
|
2317
|
+
this.stateManager.setCapturedImages({
|
|
2318
|
+
front: {
|
|
2319
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2320
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// Check if document classification is enabled (independent of detector)
|
|
2324
|
+
if (this.useDocumentClassification) {
|
|
2325
|
+
try {
|
|
2326
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2327
|
+
if (classification && classification.class === 'passport') {
|
|
2328
|
+
this.completeProcess(true);
|
|
2329
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2330
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2331
|
+
// Clear processing button when passport is detected
|
|
2332
|
+
this.processingButton = null;
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
catch (classifyError) {
|
|
2337
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
this.stateManager.updateCaptureState({
|
|
2341
|
+
step: 'back',
|
|
2342
|
+
isDetectionPaused: true,
|
|
2343
|
+
showFlipAnimation: true
|
|
2344
|
+
});
|
|
2345
|
+
this.triggerRerender();
|
|
2346
|
+
setTimeout(() => {
|
|
2347
|
+
this.stateManager.updateCaptureState({
|
|
2348
|
+
showFlipAnimation: false,
|
|
2349
|
+
isDetectionPaused: false
|
|
2350
|
+
});
|
|
2351
|
+
this.triggerRerender();
|
|
2352
|
+
this.startBackDocumentTimer();
|
|
2353
|
+
// Clear processing button after animation completes
|
|
2354
|
+
this.processingButton = null;
|
|
2355
|
+
}, 3000);
|
|
2356
|
+
}
|
|
2357
|
+
else if (captureState.step === 'back') {
|
|
2358
|
+
this.stateManager.setCapturedImages({
|
|
2359
|
+
back: {
|
|
2360
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2361
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
this.completeProcess(false);
|
|
2365
|
+
// Clear processing button after back capture completes
|
|
2366
|
+
this.processingButton = null;
|
|
2367
|
+
}
|
|
2368
|
+
// Return canvases to pool after use
|
|
2369
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2370
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2371
|
+
}
|
|
2058
2372
|
async takeScreenshot() {
|
|
2059
2373
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
2060
2374
|
return;
|
|
2061
|
-
this.logger.state('INICIANDO_CAPTURA', {
|
|
2062
|
-
captureStep: this.stateManager.getCaptureState().step,
|
|
2063
|
-
detectedBox: this.lastDetectedBox,
|
|
2064
|
-
videoResolution: {
|
|
2065
|
-
width: this.videoRef.videoWidth,
|
|
2066
|
-
height: this.videoRef.videoHeight
|
|
2067
|
-
}
|
|
2068
|
-
});
|
|
2069
2375
|
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2070
2376
|
this.triggerCaptureAnimation();
|
|
2071
|
-
//
|
|
2072
|
-
const captureCanvas =
|
|
2073
|
-
captureCanvas.width = this.videoRef.videoWidth;
|
|
2074
|
-
captureCanvas.height = this.videoRef.videoHeight;
|
|
2377
|
+
// Use pooled canvas for optimization
|
|
2378
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2075
2379
|
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2380
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2076
2381
|
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2077
2382
|
// Calculate crop coordinates
|
|
2078
2383
|
const INPUT_SIZE = 320;
|
|
@@ -2082,11 +2387,10 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2082
2387
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
2083
2388
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
2084
2389
|
const cropHeight = Math.min((this.lastDetectedBox.h * scaleY) + (2 * this.cropMargin), this.videoRef.videoHeight - cropY);
|
|
2085
|
-
//
|
|
2086
|
-
const croppedCanvas =
|
|
2087
|
-
croppedCanvas.width = cropWidth;
|
|
2088
|
-
croppedCanvas.height = cropHeight;
|
|
2390
|
+
// Use pooled canvas for cropped version
|
|
2391
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2089
2392
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2393
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2090
2394
|
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2091
2395
|
const captureState = this.stateManager.getCaptureState();
|
|
2092
2396
|
if (captureState.step === 'front') {
|
|
@@ -2098,11 +2402,15 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2098
2402
|
});
|
|
2099
2403
|
// Check if document classification is enabled
|
|
2100
2404
|
if (this.useDocumentClassification) {
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2405
|
+
try {
|
|
2406
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2407
|
+
if (classification && classification.class === 'passport') {
|
|
2408
|
+
this.completeProcess(true);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
catch (classifyError) {
|
|
2413
|
+
// No cambiar a manual aquí, solo registrar el error y continuar
|
|
2106
2414
|
}
|
|
2107
2415
|
}
|
|
2108
2416
|
this.stateManager.updateCaptureState({
|
|
@@ -2127,6 +2435,9 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2127
2435
|
});
|
|
2128
2436
|
this.completeProcess(false);
|
|
2129
2437
|
}
|
|
2438
|
+
// Return canvases to pool after use
|
|
2439
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2440
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2130
2441
|
}
|
|
2131
2442
|
triggerCaptureAnimation() {
|
|
2132
2443
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -2136,6 +2447,135 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2136
2447
|
cardOutline?.classList.remove('capturing');
|
|
2137
2448
|
}, 600);
|
|
2138
2449
|
}
|
|
2450
|
+
// Método simplificado para cambio a modo manual
|
|
2451
|
+
switchToManualMode() {
|
|
2452
|
+
// Only switch if detector is enabled and not already switched
|
|
2453
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
// Detener todo el monitoreo de performance
|
|
2457
|
+
this.stopPerformanceMonitoring();
|
|
2458
|
+
// Liberar recursos de ONNX
|
|
2459
|
+
this.releaseOnnxResources();
|
|
2460
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2461
|
+
this.resetMaskToWhite();
|
|
2462
|
+
this.detectionBoxes = [];
|
|
2463
|
+
this.hasAutoSwitchedToManual = true;
|
|
2464
|
+
this.performanceDegradedMode = true;
|
|
2465
|
+
this.showManualCaptureButton = true;
|
|
2466
|
+
this.showPerformanceMessage = true;
|
|
2467
|
+
// Hide performance message after 5 seconds
|
|
2468
|
+
setTimeout(() => {
|
|
2469
|
+
this.showPerformanceMessage = false;
|
|
2470
|
+
}, 5000);
|
|
2471
|
+
}
|
|
2472
|
+
// Método para cambiar a modo manual por errores de ONNX
|
|
2473
|
+
switchToManualModeWithWarning(warningMessage) {
|
|
2474
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2475
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
// No necesitamos liberar recursos ONNX porque no se han cargado aún
|
|
2479
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2480
|
+
this.resetMaskToWhite();
|
|
2481
|
+
this.detectionBoxes = [];
|
|
2482
|
+
// Cambiar a modo manual
|
|
2483
|
+
this.hasAutoSwitchedToManual = true;
|
|
2484
|
+
this.performanceDegradedMode = true;
|
|
2485
|
+
this.showManualCaptureButton = true;
|
|
2486
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2487
|
+
// Actualizar el estado con mensaje informativo (no error)
|
|
2488
|
+
this.updateStatus('Modo manual activado', warningMessage, 'active');
|
|
2489
|
+
// Continuar con la configuración de cámara
|
|
2490
|
+
this.continueWithCameraSetup();
|
|
2491
|
+
}
|
|
2492
|
+
async continueWithCameraSetup() {
|
|
2493
|
+
try {
|
|
2494
|
+
// Paso 2: Detectar y configurar dispositivos
|
|
2495
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
2496
|
+
try {
|
|
2497
|
+
await this.cameraService.enumerateDevices();
|
|
2498
|
+
}
|
|
2499
|
+
catch (enumerateError) {
|
|
2500
|
+
throw new Error(`Error al enumerar dispositivos: ${enumerateError.message}`);
|
|
2501
|
+
}
|
|
2502
|
+
try {
|
|
2503
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2504
|
+
}
|
|
2505
|
+
catch (cameraInfoError) {
|
|
2506
|
+
throw new Error(`Error al actualizar información de cámara: ${cameraInfoError.message}`);
|
|
2507
|
+
}
|
|
2508
|
+
// Paso 3: Configurar cámara seleccionada
|
|
2509
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
2510
|
+
let stream;
|
|
2511
|
+
try {
|
|
2512
|
+
stream = await this.cameraService.setupCamera();
|
|
2513
|
+
}
|
|
2514
|
+
catch (setupError) {
|
|
2515
|
+
throw new Error(`Error al configurar cámara: ${setupError.message}`);
|
|
2516
|
+
}
|
|
2517
|
+
// Paso 4: Inicializar video
|
|
2518
|
+
this.updateStatus('Captura manual', 'Posicione su documento y use el botón para capturar', 'active');
|
|
2519
|
+
try {
|
|
2520
|
+
await this.initializeVideoStream(stream);
|
|
2521
|
+
}
|
|
2522
|
+
catch (videoError) {
|
|
2523
|
+
throw new Error(`Error al inicializar video: ${videoError.message}`);
|
|
2524
|
+
}
|
|
2525
|
+
// Paso 5: Calibrar máscara
|
|
2526
|
+
if (this.detectionContainer) {
|
|
2527
|
+
const container = this.detectionContainer.parentElement;
|
|
2528
|
+
const rect = container.getBoundingClientRect();
|
|
2529
|
+
this.updateMaskDimensions(rect);
|
|
2530
|
+
}
|
|
2531
|
+
// Finalizar
|
|
2532
|
+
this.startTime = Date.now();
|
|
2533
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
2534
|
+
// Mostrar botón manual (ya debe estar habilitado)
|
|
2535
|
+
this.showManualCaptureButton = true;
|
|
2536
|
+
}
|
|
2537
|
+
catch (error) {
|
|
2538
|
+
this.updateStatus('Error de cámara', error.message, 'error');
|
|
2539
|
+
throw error;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
switchToManualModeWithError(errorMessage) {
|
|
2543
|
+
// Solo cambiar si el detector está habilitado y no se ha cambiado ya
|
|
2544
|
+
if (!this.useDocumentDetector || this.hasAutoSwitchedToManual) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
// Liberar recursos de ONNX inmediatamente
|
|
2548
|
+
this.releaseOnnxResources();
|
|
2549
|
+
// Resetear máscara a blanco y limpiar detecciones
|
|
2550
|
+
this.resetMaskToWhite();
|
|
2551
|
+
this.detectionBoxes = [];
|
|
2552
|
+
// Cambiar a modo manual
|
|
2553
|
+
this.hasAutoSwitchedToManual = true;
|
|
2554
|
+
this.performanceDegradedMode = true;
|
|
2555
|
+
this.showManualCaptureButton = true;
|
|
2556
|
+
this.useDocumentDetector = false; // Desactivar detector automático
|
|
2557
|
+
// Actualizar el estado con mensaje de error
|
|
2558
|
+
this.updateStatus('Modo manual activado', errorMessage, 'error');
|
|
2559
|
+
// Ocultar mensaje después de 5 segundos
|
|
2560
|
+
setTimeout(() => {
|
|
2561
|
+
this.updateStatus('Captura manual', 'Use el botón para capturar', 'ready');
|
|
2562
|
+
}, 5000);
|
|
2563
|
+
}
|
|
2564
|
+
stopPerformanceMonitoring() {
|
|
2565
|
+
// Nota: No necesitamos limpiar más variables porque ya no se usarán
|
|
2566
|
+
// El sistema automáticamente saltará la lógica de performance una vez que hasAutoSwitchedToManual = true
|
|
2567
|
+
}
|
|
2568
|
+
// Método para liberar recursos de ONNX cuando se cambia a modo manual
|
|
2569
|
+
releaseOnnxResources() {
|
|
2570
|
+
try {
|
|
2571
|
+
// Liberar recursos del servicio de detección
|
|
2572
|
+
if (this.detectionService) {
|
|
2573
|
+
this.detectionService.cleanup();
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
catch (error) {
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2139
2579
|
completeProcess(skippedBack = false) {
|
|
2140
2580
|
this.stateManager.updateCaptureState({
|
|
2141
2581
|
step: 'completed',
|
|
@@ -2155,11 +2595,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2155
2595
|
setTimeout(() => {
|
|
2156
2596
|
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
2157
2597
|
}, 3000);
|
|
2158
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
2159
|
-
skippedBack,
|
|
2160
|
-
totalImages: capturedImages.metadata.totalImages,
|
|
2161
|
-
timestamp: new Date().toISOString()
|
|
2162
|
-
});
|
|
2163
2598
|
}
|
|
2164
2599
|
stopDetection() {
|
|
2165
2600
|
if (this.animationId) {
|
|
@@ -2168,7 +2603,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2168
2603
|
}
|
|
2169
2604
|
this.clearBackDocumentTimer();
|
|
2170
2605
|
this.detectionBoxes = [];
|
|
2171
|
-
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
2172
2606
|
}
|
|
2173
2607
|
startBackDocumentTimer() {
|
|
2174
2608
|
if (!this.enableBackDocumentTimer)
|
|
@@ -2197,6 +2631,15 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2197
2631
|
return; // Don't toggle if switching camera
|
|
2198
2632
|
this.showCameraSelector = !this.showCameraSelector;
|
|
2199
2633
|
}
|
|
2634
|
+
async updateCameraInfoWithAutofocus() {
|
|
2635
|
+
try {
|
|
2636
|
+
const cameraInfo = await this.cameraService.getCameraInfo();
|
|
2637
|
+
this.cameraInfoWithAutofocus = cameraInfo;
|
|
2638
|
+
}
|
|
2639
|
+
catch (error) {
|
|
2640
|
+
// Keep existing state if update fails
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2200
2643
|
async handleCameraSwitch(cameraId) {
|
|
2201
2644
|
if (this.isSwitchingCamera)
|
|
2202
2645
|
return; // Prevent multiple simultaneous switches
|
|
@@ -2204,10 +2647,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2204
2647
|
// Close the selector immediately when user selects a camera
|
|
2205
2648
|
this.showCameraSelector = false;
|
|
2206
2649
|
this.isSwitchingCamera = true;
|
|
2207
|
-
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2208
|
-
from: this.cameraService.getSelectedCameraId(),
|
|
2209
|
-
to: cameraId
|
|
2210
|
-
});
|
|
2211
2650
|
// Stop current video stream
|
|
2212
2651
|
if (this.videoStream) {
|
|
2213
2652
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
@@ -2218,21 +2657,17 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2218
2657
|
const newStream = await this.cameraService.setupCamera();
|
|
2219
2658
|
// Update video element
|
|
2220
2659
|
await this.initializeVideoStream(newStream);
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2224
|
-
});
|
|
2660
|
+
// Update camera info with autofocus data
|
|
2661
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2225
2662
|
}
|
|
2226
2663
|
catch (error) {
|
|
2227
|
-
this.logger.error('Error al cambiar cámara:', error);
|
|
2228
2664
|
// Try to restore previous camera if switch failed
|
|
2229
2665
|
try {
|
|
2230
2666
|
const fallbackStream = await this.cameraService.setupCamera();
|
|
2231
2667
|
await this.initializeVideoStream(fallbackStream);
|
|
2232
2668
|
}
|
|
2233
2669
|
catch (fallbackError) {
|
|
2234
|
-
this.
|
|
2235
|
-
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2670
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
|
|
2236
2671
|
}
|
|
2237
2672
|
}
|
|
2238
2673
|
finally {
|
|
@@ -2254,6 +2689,13 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2254
2689
|
this.detectionBoxes = [];
|
|
2255
2690
|
this.alignmentStartTime = undefined;
|
|
2256
2691
|
this.hasDocumentDetected = false;
|
|
2692
|
+
// Reset sistema simplificado
|
|
2693
|
+
this.processedFramesCount = 0;
|
|
2694
|
+
this.slowFrameCount = 0;
|
|
2695
|
+
// Reset performance degradation state
|
|
2696
|
+
this.hasAutoSwitchedToManual = false;
|
|
2697
|
+
this.performanceDegradedMode = false;
|
|
2698
|
+
this.showPerformanceMessage = false;
|
|
2257
2699
|
if (this.alignmentTimer) {
|
|
2258
2700
|
clearTimeout(this.alignmentTimer);
|
|
2259
2701
|
this.alignmentTimer = undefined;
|
|
@@ -2273,7 +2715,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2273
2715
|
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
2274
2716
|
}
|
|
2275
2717
|
this.isMaskReady = false;
|
|
2276
|
-
this.updateStatus('
|
|
2718
|
+
this.updateStatus('Captura finalizada', '', 'ready');
|
|
2277
2719
|
this.detectionBoxes = [];
|
|
2278
2720
|
this.cleanup();
|
|
2279
2721
|
}
|
|
@@ -2284,7 +2726,6 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2284
2726
|
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2285
2727
|
this.updatePerformanceMetrics();
|
|
2286
2728
|
}, 500);
|
|
2287
|
-
this.logger.debug('Monitor de performance inicializado');
|
|
2288
2729
|
}
|
|
2289
2730
|
updatePerformanceMetrics() {
|
|
2290
2731
|
const currentTime = performance.now();
|
|
@@ -2327,14 +2768,93 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2327
2768
|
if (detectionsFound > 0) {
|
|
2328
2769
|
this.performanceMetrics.successfulDetections++;
|
|
2329
2770
|
}
|
|
2771
|
+
// Update performance history for adaptive frame skipping
|
|
2772
|
+
this.performanceHistory.push(processingTime);
|
|
2773
|
+
if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
|
|
2774
|
+
this.performanceHistory.shift();
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
// Adaptive frame skipping based on performance
|
|
2778
|
+
getAdaptiveFrameSkip() {
|
|
2779
|
+
// Base conditions for high frame skip
|
|
2780
|
+
if (this.consecutiveFailures > 20)
|
|
2781
|
+
return this.MAX_FRAME_SKIP;
|
|
2782
|
+
if (this.performanceMetrics.inferenceTime > 200)
|
|
2783
|
+
return 6;
|
|
2784
|
+
if (this.performanceMetrics.memoryUsage > 200)
|
|
2785
|
+
return 5;
|
|
2786
|
+
// Calculate average processing time
|
|
2787
|
+
if (this.performanceHistory.length > 0) {
|
|
2788
|
+
const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
|
|
2789
|
+
if (avgProcessingTime > 100)
|
|
2790
|
+
return 4;
|
|
2791
|
+
if (avgProcessingTime > 80)
|
|
2792
|
+
return 3;
|
|
2793
|
+
if (avgProcessingTime > 60)
|
|
2794
|
+
return this.BASE_FRAME_SKIP + 1;
|
|
2795
|
+
}
|
|
2796
|
+
// Low memory devices get higher frame skip
|
|
2797
|
+
if (this.performanceMetrics.memoryUsage > 150)
|
|
2798
|
+
return 4;
|
|
2799
|
+
// Performance is good, use base frame skip
|
|
2800
|
+
return this.BASE_FRAME_SKIP;
|
|
2801
|
+
}
|
|
2802
|
+
// Método para resetear la máscara a color blanco
|
|
2803
|
+
resetMaskToWhite() {
|
|
2804
|
+
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
2805
|
+
const corners = this.el.shadowRoot?.querySelectorAll('.corner');
|
|
2806
|
+
const topSide = this.el.shadowRoot?.querySelector('.side-top');
|
|
2807
|
+
const rightSide = this.el.shadowRoot?.querySelector('.side-right');
|
|
2808
|
+
const bottomSide = this.el.shadowRoot?.querySelector('.side-bottom');
|
|
2809
|
+
const leftSide = this.el.shadowRoot?.querySelector('.side-left');
|
|
2810
|
+
// Remover todas las clases de alineación y estado perfecto
|
|
2811
|
+
cardOutline?.classList.remove('perfect-match');
|
|
2812
|
+
corners?.forEach(corner => corner.classList.remove('perfect-match'));
|
|
2813
|
+
topSide?.classList.remove('aligned');
|
|
2814
|
+
rightSide?.classList.remove('aligned');
|
|
2815
|
+
bottomSide?.classList.remove('aligned');
|
|
2816
|
+
leftSide?.classList.remove('aligned');
|
|
2817
|
+
// Resetear estado de alineación
|
|
2818
|
+
this.sideAlignment = { top: false, right: false, bottom: false, left: false };
|
|
2819
|
+
}
|
|
2820
|
+
// Canvas pool management for screenshots
|
|
2821
|
+
getPooledCanvas(width, height) {
|
|
2822
|
+
// Try to find a canvas with matching dimensions
|
|
2823
|
+
const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
|
|
2824
|
+
if (matchingIndex !== -1) {
|
|
2825
|
+
return this.canvasPool.splice(matchingIndex, 1)[0];
|
|
2826
|
+
}
|
|
2827
|
+
// If no matching canvas, try to reuse any canvas and resize
|
|
2828
|
+
if (this.canvasPool.length > 0) {
|
|
2829
|
+
const canvas = this.canvasPool.pop();
|
|
2830
|
+
canvas.width = width;
|
|
2831
|
+
canvas.height = height;
|
|
2832
|
+
return canvas;
|
|
2833
|
+
}
|
|
2834
|
+
// Create new canvas if pool is empty
|
|
2835
|
+
const canvas = document.createElement('canvas');
|
|
2836
|
+
canvas.width = width;
|
|
2837
|
+
canvas.height = height;
|
|
2838
|
+
return canvas;
|
|
2839
|
+
}
|
|
2840
|
+
returnCanvasToPool(canvas) {
|
|
2841
|
+
// Only return to pool if not at max capacity
|
|
2842
|
+
if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
|
|
2843
|
+
// Clear the canvas before returning to pool
|
|
2844
|
+
const ctx = canvas.getContext('2d');
|
|
2845
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2846
|
+
this.canvasPool.push(canvas);
|
|
2847
|
+
}
|
|
2848
|
+
// If pool is full, let the canvas be garbage collected
|
|
2330
2849
|
}
|
|
2331
2850
|
static get style() { return myComponentCss; }
|
|
2332
|
-
}, [
|
|
2851
|
+
}, [257, "jaak-stamps", {
|
|
2333
2852
|
"debug": [4],
|
|
2334
2853
|
"alignmentTolerance": [2, "alignment-tolerance"],
|
|
2335
2854
|
"maskSize": [2, "mask-size"],
|
|
2336
2855
|
"cropMargin": [2, "crop-margin"],
|
|
2337
2856
|
"useDocumentClassification": [4, "use-document-classification"],
|
|
2857
|
+
"useDocumentDetector": [4, "use-document-detector"],
|
|
2338
2858
|
"preferredCamera": [1, "preferred-camera"],
|
|
2339
2859
|
"captureDelay": [2, "capture-delay"],
|
|
2340
2860
|
"enableBackDocumentTimer": [4, "enable-back-document-timer"],
|
|
@@ -2346,9 +2866,15 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2346
2866
|
"showCameraSelector": [32],
|
|
2347
2867
|
"isSwitchingCamera": [32],
|
|
2348
2868
|
"hasDocumentDetected": [32],
|
|
2869
|
+
"cameraInfoWithAutofocus": [32],
|
|
2349
2870
|
"currentStatus": [32],
|
|
2350
2871
|
"performanceData": [32],
|
|
2351
2872
|
"backDocumentTimerRemaining": [32],
|
|
2873
|
+
"showManualCaptureButton": [32],
|
|
2874
|
+
"performanceDegradedMode": [32],
|
|
2875
|
+
"showPerformanceMessage": [32],
|
|
2876
|
+
"captureStateVersion": [32],
|
|
2877
|
+
"processingButton": [32],
|
|
2352
2878
|
"getCapturedImages": [64],
|
|
2353
2879
|
"isProcessCompleted": [64],
|
|
2354
2880
|
"startCapture": [64],
|
|
@@ -2360,9 +2886,7 @@ const JaakStamps$1 = /*@__PURE__*/ proxyCustomElement(class JaakStamps extends H
|
|
|
2360
2886
|
"getCameraInfo": [64],
|
|
2361
2887
|
"setPreferredCamera": [64],
|
|
2362
2888
|
"setCaptureDelay": [64],
|
|
2363
|
-
"getCaptureDelay": [64]
|
|
2364
|
-
"setTorchEnabled": [64],
|
|
2365
|
-
"focusAtPoint": [64]
|
|
2889
|
+
"getCaptureDelay": [64]
|
|
2366
2890
|
}]);
|
|
2367
2891
|
function defineCustomElement$1() {
|
|
2368
2892
|
if (typeof customElements === "undefined") {
|