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