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