@jaak.ai/stamps 2.0.0 → 2.1.0-dev.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/jaak-stamps-webcomponent.cjs.js +1 -1
- package/dist/cjs/jaak-stamps.cjs.entry.js +759 -623
- 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 +1 -1
- package/dist/collection/components/my-component/my-component.css +178 -11
- package/dist/collection/components/my-component/my-component.js +566 -250
- package/dist/collection/components/my-component/my-component.js.map +1 -1
- package/dist/collection/services/CameraService.js +156 -344
- package/dist/collection/services/CameraService.js.map +1 -1
- package/dist/collection/services/DetectionService.js +68 -38
- 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/jaak-stamps.js +763 -626
- package/dist/components/jaak-stamps.js.map +1 -1
- package/dist/esm/jaak-stamps-webcomponent.js +1 -1
- package/dist/esm/jaak-stamps.entry.js +759 -623
- package/dist/esm/jaak-stamps.entry.js.map +1 -1
- package/dist/esm/loader.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps-webcomponent.esm.js +1 -1
- package/dist/jaak-stamps-webcomponent/jaak-stamps.entry.esm.js.map +1 -1
- package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js +2 -0
- package/dist/jaak-stamps-webcomponent/p-41e88688.entry.js.map +1 -0
- package/dist/types/components/my-component/my-component.d.ts +68 -17
- package/dist/types/components.d.ts +14 -8
- package/dist/types/services/CameraService.d.ts +12 -14
- package/dist/types/services/DetectionService.d.ts +10 -3
- package/dist/types/services/ServiceContainer.d.ts +1 -2
- package/dist/types/services/interfaces/ICameraService.d.ts +3 -12
- package/dist/types/types/component-types.d.ts +3 -0
- package/package.json +2 -2
- 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/jaak-stamps-webcomponent/p-2264b5b4.entry.js +0 -2
- package/dist/jaak-stamps-webcomponent/p-2264b5b4.entry.js.map +0 -1
- package/dist/types/services/LoggerService.d.ts +0 -12
- package/dist/types/services/interfaces/ILogger.d.ts +0 -8
|
@@ -1,45 +1,5 @@
|
|
|
1
1
|
import { r as registerInstance, c as createEvent, a as getElement, h } from './index-BP1Q4KOg.js';
|
|
2
2
|
|
|
3
|
-
class LoggerService {
|
|
4
|
-
debugMode;
|
|
5
|
-
constructor(debug = false) {
|
|
6
|
-
this.debugMode = debug;
|
|
7
|
-
}
|
|
8
|
-
setDebugMode(debug) {
|
|
9
|
-
this.debugMode = debug;
|
|
10
|
-
}
|
|
11
|
-
info(...args) {
|
|
12
|
-
if (this.debugMode) {
|
|
13
|
-
console.log(`[JAAK-STAMPS] [INFO] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
warn(...args) {
|
|
17
|
-
if (this.debugMode) {
|
|
18
|
-
console.warn(`[JAAK-STAMPS] [WARN] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
error(...args) {
|
|
22
|
-
if (this.debugMode) {
|
|
23
|
-
console.error(`[JAAK-STAMPS] [ERROR] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
debug(...args) {
|
|
27
|
-
if (this.debugMode) {
|
|
28
|
-
console.debug(`[JAAK-STAMPS] [DEBUG] [${new Date().toLocaleTimeString()}]`, ...args);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
state(state, data) {
|
|
32
|
-
if (this.debugMode) {
|
|
33
|
-
console.log(`[JAAK-STAMPS] [STATE] [${new Date().toLocaleTimeString()}] ${state}`, data || '');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
performance(operation, duration) {
|
|
37
|
-
if (this.debugMode) {
|
|
38
|
-
console.log(`[JAAK-STAMPS] [PERF] [${new Date().toLocaleTimeString()}] ${operation}: ${duration}ms`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
3
|
class EventBusService {
|
|
44
4
|
events = new Map();
|
|
45
5
|
on(event, callback) {
|
|
@@ -65,7 +25,7 @@ class EventBusService {
|
|
|
65
25
|
callback(data);
|
|
66
26
|
}
|
|
67
27
|
catch (error) {
|
|
68
|
-
|
|
28
|
+
// Silently handle callback errors
|
|
69
29
|
}
|
|
70
30
|
});
|
|
71
31
|
}
|
|
@@ -192,16 +152,20 @@ class StateManagerService {
|
|
|
192
152
|
}
|
|
193
153
|
|
|
194
154
|
class CameraService {
|
|
195
|
-
logger;
|
|
196
155
|
eventBus;
|
|
197
156
|
availableCameras = [];
|
|
198
157
|
selectedCameraId = null;
|
|
199
158
|
deviceType = 'desktop';
|
|
200
159
|
preferredCameraFacing = null;
|
|
201
160
|
preferredCamera = 'auto';
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
161
|
+
// Optimization: Static caches for performance
|
|
162
|
+
static cameraCapabilitiesCache = new Map();
|
|
163
|
+
static deviceEnumerationCache = null;
|
|
164
|
+
static CACHE_DURATION = 30000; // 30 seconds
|
|
165
|
+
// Stream management for optimization
|
|
166
|
+
currentStreamPromise;
|
|
167
|
+
lastStreamConstraints;
|
|
168
|
+
constructor(eventBus, preferredCamera = 'auto') {
|
|
205
169
|
this.eventBus = eventBus;
|
|
206
170
|
this.preferredCamera = preferredCamera;
|
|
207
171
|
}
|
|
@@ -218,18 +182,21 @@ class CameraService {
|
|
|
218
182
|
else {
|
|
219
183
|
this.deviceType = 'desktop';
|
|
220
184
|
}
|
|
221
|
-
this.logger.state('DISPOSITIVO_DETECTADO', {
|
|
222
|
-
deviceType: this.deviceType,
|
|
223
|
-
userAgent: navigator.userAgent,
|
|
224
|
-
screenDimensions: { width: window.innerWidth, height: window.innerHeight }
|
|
225
|
-
});
|
|
226
185
|
return this.deviceType;
|
|
227
186
|
}
|
|
228
187
|
async enumerateDevices() {
|
|
229
188
|
try {
|
|
189
|
+
// Check cache first for optimization
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
if (CameraService.deviceEnumerationCache &&
|
|
192
|
+
(now - CameraService.deviceEnumerationCache.timestamp) < CameraService.CACHE_DURATION) {
|
|
193
|
+
this.availableCameras = CameraService.deviceEnumerationCache.devices;
|
|
194
|
+
await this.setInitialCameraPreference();
|
|
195
|
+
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
196
|
+
return this.availableCameras;
|
|
197
|
+
}
|
|
230
198
|
const permissionStatus = await this.checkCameraPermission();
|
|
231
199
|
if (permissionStatus === 'denied') {
|
|
232
|
-
this.logger.error('Permiso de cámara denegado por el usuario');
|
|
233
200
|
return [];
|
|
234
201
|
}
|
|
235
202
|
if (permissionStatus === 'prompt') {
|
|
@@ -238,24 +205,16 @@ class CameraService {
|
|
|
238
205
|
}
|
|
239
206
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
240
207
|
this.availableCameras = devices.filter(device => device.kind === 'videoinput');
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
});
|
|
248
|
-
this.setInitialCameraPreference();
|
|
249
|
-
this.logger.state('PREFERENCIA_INICIAL_APLICADA', {
|
|
250
|
-
selectedCameraId: this.selectedCameraId,
|
|
251
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
252
|
-
preferredCamera: this.preferredCamera
|
|
253
|
-
});
|
|
208
|
+
// Cache the results for optimization
|
|
209
|
+
CameraService.deviceEnumerationCache = {
|
|
210
|
+
devices: this.availableCameras,
|
|
211
|
+
timestamp: now
|
|
212
|
+
};
|
|
213
|
+
await this.setInitialCameraPreference();
|
|
254
214
|
this.eventBus.emit('camera-changed', this.selectedCameraId);
|
|
255
215
|
return this.availableCameras;
|
|
256
216
|
}
|
|
257
217
|
catch (error) {
|
|
258
|
-
this.logger.error('Error al enumerar cámaras disponibles:', error);
|
|
259
218
|
this.handleCameraPermissionError(error);
|
|
260
219
|
return [];
|
|
261
220
|
}
|
|
@@ -276,8 +235,6 @@ class CameraService {
|
|
|
276
235
|
}
|
|
277
236
|
this.selectedCameraId = cameraId;
|
|
278
237
|
this.updatePreferredFacing(camera);
|
|
279
|
-
this.isManuallySelected = true; // Mark as manually selected
|
|
280
|
-
this.savePreference();
|
|
281
238
|
this.eventBus.emit('camera-changed', cameraId);
|
|
282
239
|
}
|
|
283
240
|
getPreferredFacing() {
|
|
@@ -286,14 +243,20 @@ class CameraService {
|
|
|
286
243
|
async setupCamera(constraints) {
|
|
287
244
|
try {
|
|
288
245
|
const finalConstraints = constraints || await this.getMaxResolution();
|
|
289
|
-
|
|
246
|
+
// Optimization: Check if we can reuse existing stream with same constraints
|
|
247
|
+
if (this.currentStreamPromise && this.constraintsEqual(finalConstraints, this.lastStreamConstraints)) {
|
|
248
|
+
return await this.currentStreamPromise;
|
|
249
|
+
}
|
|
250
|
+
// Create new stream promise for lazy loading
|
|
251
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
290
252
|
video: finalConstraints,
|
|
291
253
|
audio: false
|
|
292
254
|
});
|
|
255
|
+
this.lastStreamConstraints = finalConstraints;
|
|
256
|
+
const stream = await this.currentStreamPromise;
|
|
293
257
|
return stream;
|
|
294
258
|
}
|
|
295
259
|
catch (error) {
|
|
296
|
-
this.logger.error('Error en setupCamera, intentando con restricciones básicas:', error);
|
|
297
260
|
// Fallback to basic constraints if getMaxResolution fails
|
|
298
261
|
const basicConstraints = {
|
|
299
262
|
width: { ideal: 1280 },
|
|
@@ -303,65 +266,21 @@ class CameraService {
|
|
|
303
266
|
if (this.deviceType !== 'desktop' && this.preferredCameraFacing) {
|
|
304
267
|
basicConstraints.facingMode = this.preferredCameraFacing;
|
|
305
268
|
}
|
|
306
|
-
this.
|
|
307
|
-
return await navigator.mediaDevices.getUserMedia({
|
|
269
|
+
this.currentStreamPromise = navigator.mediaDevices.getUserMedia({
|
|
308
270
|
video: basicConstraints,
|
|
309
271
|
audio: false
|
|
310
272
|
});
|
|
273
|
+
this.lastStreamConstraints = basicConstraints;
|
|
274
|
+
return await this.currentStreamPromise;
|
|
311
275
|
}
|
|
312
276
|
}
|
|
313
277
|
async switchCamera(cameraId) {
|
|
314
278
|
const selectedCamera = this.availableCameras.find(cam => cam.deviceId === cameraId);
|
|
315
279
|
if (!selectedCamera) {
|
|
316
|
-
this.logger.warn('Cámara seleccionada no encontrada, re-enumerando dispositivos...');
|
|
317
280
|
await this.enumerateDevices();
|
|
318
281
|
return;
|
|
319
282
|
}
|
|
320
283
|
await this.setSelectedCamera(cameraId);
|
|
321
|
-
this.logger.state('CAMARA_CAMBIADA', {
|
|
322
|
-
label: selectedCamera.label,
|
|
323
|
-
deviceId: selectedCamera.deviceId,
|
|
324
|
-
isManuallySelected: this.isManuallySelected
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
async flipToNextCamera() {
|
|
328
|
-
if (!this.isMultipleCamerasAvailable())
|
|
329
|
-
return;
|
|
330
|
-
const currentIndex = this.availableCameras.findIndex(camera => camera.deviceId === this.selectedCameraId);
|
|
331
|
-
const nextIndex = (currentIndex + 1) % this.availableCameras.length;
|
|
332
|
-
const nextCamera = this.availableCameras[nextIndex];
|
|
333
|
-
await this.switchCamera(nextCamera.deviceId);
|
|
334
|
-
}
|
|
335
|
-
savePreference() {
|
|
336
|
-
try {
|
|
337
|
-
const preference = {
|
|
338
|
-
cameraId: this.selectedCameraId,
|
|
339
|
-
facing: this.preferredCameraFacing,
|
|
340
|
-
timestamp: Date.now()
|
|
341
|
-
};
|
|
342
|
-
localStorage.setItem('jaak-stamps-camera-preference', JSON.stringify(preference));
|
|
343
|
-
this.logger.state('PREFERENCIA_CAMARA_GUARDADA', preference);
|
|
344
|
-
}
|
|
345
|
-
catch (error) {
|
|
346
|
-
this.logger.warn('Error al guardar preferencia de cámara:', error);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
loadPreference() {
|
|
350
|
-
try {
|
|
351
|
-
const saved = localStorage.getItem('jaak-stamps-camera-preference');
|
|
352
|
-
if (saved) {
|
|
353
|
-
const preference = JSON.parse(saved);
|
|
354
|
-
const isStillAvailable = this.availableCameras.some(camera => camera.deviceId === preference.cameraId);
|
|
355
|
-
if (isStillAvailable) {
|
|
356
|
-
this.selectedCameraId = preference.cameraId;
|
|
357
|
-
this.preferredCameraFacing = preference.facing;
|
|
358
|
-
this.logger.state('PREFERENCIA_CAMARA_CARGADA', preference);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
this.logger.warn('Error al cargar preferencia de cámara:', error);
|
|
364
|
-
}
|
|
365
284
|
}
|
|
366
285
|
isRearCamera(stream) {
|
|
367
286
|
const videoTrack = stream.getVideoTracks()[0];
|
|
@@ -370,13 +289,18 @@ class CameraService {
|
|
|
370
289
|
const settings = videoTrack.getSettings();
|
|
371
290
|
return settings.facingMode === 'environment';
|
|
372
291
|
}
|
|
373
|
-
getCameraInfo() {
|
|
374
|
-
|
|
375
|
-
|
|
292
|
+
async getCameraInfo() {
|
|
293
|
+
const availableCamerasWithAutofocus = await Promise.all(this.availableCameras.map(async (camera) => {
|
|
294
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
295
|
+
return {
|
|
376
296
|
id: camera.deviceId,
|
|
377
297
|
label: camera.label || 'Unknown Camera',
|
|
378
|
-
selected: camera.deviceId === this.selectedCameraId
|
|
379
|
-
|
|
298
|
+
selected: camera.deviceId === this.selectedCameraId,
|
|
299
|
+
hasAutofocus
|
|
300
|
+
};
|
|
301
|
+
}));
|
|
302
|
+
return {
|
|
303
|
+
availableCameras: availableCamerasWithAutofocus,
|
|
380
304
|
selectedCameraId: this.selectedCameraId,
|
|
381
305
|
deviceType: this.deviceType,
|
|
382
306
|
isMultipleCamerasAvailable: this.isMultipleCamerasAvailable(),
|
|
@@ -392,7 +316,6 @@ class CameraService {
|
|
|
392
316
|
return permission.state;
|
|
393
317
|
}
|
|
394
318
|
catch (error) {
|
|
395
|
-
this.logger.warn('No se pudo verificar permisos de cámara:', error);
|
|
396
319
|
return 'prompt';
|
|
397
320
|
}
|
|
398
321
|
}
|
|
@@ -400,35 +323,48 @@ class CameraService {
|
|
|
400
323
|
this.availableCameras = [];
|
|
401
324
|
this.eventBus.emit('error', new Error(`Camera permission error: ${error.message}`));
|
|
402
325
|
}
|
|
403
|
-
setInitialCameraPreference() {
|
|
326
|
+
async setInitialCameraPreference() {
|
|
404
327
|
if (this.availableCameras.length === 0)
|
|
405
328
|
return;
|
|
406
|
-
// Clear any existing localStorage preferences to ensure fresh start
|
|
407
|
-
localStorage.removeItem('jaak-stamps-camera-preference');
|
|
408
|
-
this.logger.state('APLICANDO_PREFERENCIA_INICIAL', {
|
|
409
|
-
preferredCamera: this.preferredCamera,
|
|
410
|
-
availableCamerasCount: this.availableCameras.length
|
|
411
|
-
});
|
|
412
|
-
this.isManuallySelected = false; // Reset manual selection flag
|
|
413
329
|
if (this.preferredCamera === 'front') {
|
|
414
|
-
this.selectFrontCamera();
|
|
330
|
+
await this.selectFrontCamera();
|
|
415
331
|
}
|
|
416
332
|
else if (this.preferredCamera === 'back') {
|
|
417
|
-
this.selectBackCamera();
|
|
333
|
+
await this.selectBackCamera();
|
|
418
334
|
}
|
|
419
335
|
else {
|
|
420
|
-
this.selectAutoCamera();
|
|
336
|
+
await this.selectAutoCamera();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async checkCameraAutofocus(deviceId) {
|
|
340
|
+
try {
|
|
341
|
+
// Check cache first for optimization
|
|
342
|
+
if (CameraService.cameraCapabilitiesCache.has(deviceId)) {
|
|
343
|
+
const cached = CameraService.cameraCapabilitiesCache.get(deviceId);
|
|
344
|
+
return typeof cached === 'boolean' ? cached : false;
|
|
345
|
+
}
|
|
346
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
347
|
+
video: { deviceId: { exact: deviceId } }
|
|
348
|
+
});
|
|
349
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
350
|
+
const capabilities = videoTrack.getCapabilities();
|
|
351
|
+
stream.getTracks().forEach(track => track.stop());
|
|
352
|
+
const hasFocusMode = capabilities.focusMode && Array.isArray(capabilities.focusMode) && capabilities.focusMode.length > 0;
|
|
353
|
+
const hasAutofocus = hasFocusMode && (capabilities.focusMode.includes('continuous') ||
|
|
354
|
+
capabilities.focusMode.includes('auto'));
|
|
355
|
+
// Cache the result for future use
|
|
356
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, hasAutofocus);
|
|
357
|
+
return hasAutofocus;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
// Cache negative result to avoid repeated failures
|
|
361
|
+
CameraService.cameraCapabilitiesCache.set(deviceId, false);
|
|
362
|
+
return false;
|
|
421
363
|
}
|
|
422
364
|
}
|
|
423
|
-
selectFrontCamera() {
|
|
365
|
+
async selectFrontCamera() {
|
|
424
366
|
this.preferredCameraFacing = 'user';
|
|
425
|
-
this.
|
|
426
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
427
|
-
id: cam.deviceId,
|
|
428
|
-
label: cam.label
|
|
429
|
-
}))
|
|
430
|
-
});
|
|
431
|
-
const frontCamera = this.availableCameras.find(camera => {
|
|
367
|
+
const frontCameras = this.availableCameras.filter(camera => {
|
|
432
368
|
const label = camera.label.toLowerCase();
|
|
433
369
|
return label.includes('front') ||
|
|
434
370
|
label.includes('user') ||
|
|
@@ -436,24 +372,24 @@ class CameraService {
|
|
|
436
372
|
label.includes('frontal') ||
|
|
437
373
|
(!label.includes('back') && !label.includes('rear') && !label.includes('environment'));
|
|
438
374
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
375
|
+
let selectedCamera = null;
|
|
376
|
+
if (frontCameras.length > 0) {
|
|
377
|
+
for (const camera of frontCameras) {
|
|
378
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
379
|
+
if (hasAutofocus) {
|
|
380
|
+
selectedCamera = camera;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!selectedCamera) {
|
|
385
|
+
selectedCamera = frontCameras[0];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
446
389
|
}
|
|
447
|
-
selectBackCamera() {
|
|
390
|
+
async selectBackCamera() {
|
|
448
391
|
this.preferredCameraFacing = 'environment';
|
|
449
|
-
this.
|
|
450
|
-
availableCameras: this.availableCameras.map(cam => ({
|
|
451
|
-
id: cam.deviceId,
|
|
452
|
-
label: cam.label
|
|
453
|
-
}))
|
|
454
|
-
});
|
|
455
|
-
// Try to find rear camera with multiple detection methods
|
|
456
|
-
let backCamera = this.availableCameras.find(camera => {
|
|
392
|
+
const backCameras = this.availableCameras.filter(camera => {
|
|
457
393
|
const label = camera.label.toLowerCase();
|
|
458
394
|
return label.includes('back') ||
|
|
459
395
|
label.includes('rear') ||
|
|
@@ -461,34 +397,33 @@ class CameraService {
|
|
|
461
397
|
label.includes('trasera') ||
|
|
462
398
|
label.includes('posterior');
|
|
463
399
|
});
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
400
|
+
let selectedCamera = null;
|
|
401
|
+
if (backCameras.length > 0) {
|
|
402
|
+
for (const camera of backCameras) {
|
|
403
|
+
const hasAutofocus = await this.checkCameraAutofocus(camera.deviceId);
|
|
404
|
+
if (hasAutofocus) {
|
|
405
|
+
selectedCamera = camera;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!selectedCamera) {
|
|
410
|
+
selectedCamera = backCameras[0];
|
|
411
|
+
}
|
|
471
412
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
413
|
+
else if (this.availableCameras.length > 1) {
|
|
414
|
+
const lastCamera = this.availableCameras[this.availableCameras.length - 1];
|
|
415
|
+
const hasAutofocus = await this.checkCameraAutofocus(lastCamera.deviceId);
|
|
416
|
+
selectedCamera = hasAutofocus ? lastCamera : this.availableCameras[this.availableCameras.length - 1];
|
|
417
|
+
}
|
|
418
|
+
this.selectedCameraId = selectedCamera ? selectedCamera.deviceId : this.availableCameras[0].deviceId;
|
|
478
419
|
}
|
|
479
|
-
selectAutoCamera() {
|
|
420
|
+
async selectAutoCamera() {
|
|
480
421
|
if (this.deviceType === 'mobile' || this.deviceType === 'tablet') {
|
|
481
|
-
this.selectBackCamera();
|
|
482
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_MOBILE', { type: 'rear' });
|
|
422
|
+
await this.selectBackCamera();
|
|
483
423
|
}
|
|
484
424
|
else {
|
|
485
425
|
// For desktop, prefer front camera (usually built-in webcam)
|
|
486
|
-
this.selectFrontCamera();
|
|
487
|
-
this.logger.state('CAMARA_AUTO_SELECCIONADA_DESKTOP', {
|
|
488
|
-
type: 'front',
|
|
489
|
-
label: this.availableCameras[0].label,
|
|
490
|
-
deviceId: this.selectedCameraId
|
|
491
|
-
});
|
|
426
|
+
await this.selectFrontCamera();
|
|
492
427
|
}
|
|
493
428
|
}
|
|
494
429
|
updatePreferredFacing(camera) {
|
|
@@ -504,40 +439,19 @@ class CameraService {
|
|
|
504
439
|
await this.detectDeviceType();
|
|
505
440
|
}
|
|
506
441
|
const videoConstraints = {};
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
preferredCameraFacing: this.preferredCameraFacing,
|
|
510
|
-
preferredCamera: this.preferredCamera,
|
|
511
|
-
deviceType: this.deviceType,
|
|
512
|
-
isManuallySelected: this.isManuallySelected
|
|
513
|
-
});
|
|
514
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
515
|
-
// When manually selected, use deviceId for exact camera selection
|
|
442
|
+
// Priority 1: Always use deviceId if available (ensures specific camera with autofocus)
|
|
443
|
+
if (this.selectedCameraId) {
|
|
516
444
|
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
517
|
-
this.logger.state('USANDO_CAMARA_MANUAL', {
|
|
518
|
-
deviceId: this.selectedCameraId
|
|
519
|
-
});
|
|
520
445
|
}
|
|
446
|
+
// Priority 2: Use facingMode as fallback when no specific camera is selected
|
|
521
447
|
else if (this.preferredCameraFacing) {
|
|
522
|
-
// Use facingMode for initial preference-based selection
|
|
523
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
524
448
|
const facingConstraint = this.deviceType === 'desktop'
|
|
525
449
|
? { ideal: this.preferredCameraFacing }
|
|
526
450
|
: { exact: this.preferredCameraFacing };
|
|
527
451
|
videoConstraints.facingMode = facingConstraint;
|
|
528
|
-
this.logger.state('USANDO_FACING_CONSTRAINT', {
|
|
529
|
-
facingMode: this.preferredCameraFacing,
|
|
530
|
-
constraintType: this.deviceType === 'desktop' ? 'ideal' : 'exact'
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
else if (this.selectedCameraId) {
|
|
534
|
-
videoConstraints.deviceId = { exact: this.selectedCameraId };
|
|
535
|
-
this.logger.state('USANDO_CAMARA_SELECCIONADA', {
|
|
536
|
-
deviceId: this.selectedCameraId
|
|
537
|
-
});
|
|
538
452
|
}
|
|
453
|
+
// Priority 3: Fallback to preferredCamera setting
|
|
539
454
|
else {
|
|
540
|
-
// Use preferredCamera setting to determine default facing mode
|
|
541
455
|
if (this.preferredCamera === 'front') {
|
|
542
456
|
videoConstraints.facingMode = "user";
|
|
543
457
|
}
|
|
@@ -545,24 +459,25 @@ class CameraService {
|
|
|
545
459
|
videoConstraints.facingMode = "environment";
|
|
546
460
|
}
|
|
547
461
|
else {
|
|
548
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
549
462
|
videoConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
550
463
|
}
|
|
551
|
-
this.logger.state('USANDO_PREFERENCIA_CONFIGURADA', {
|
|
552
|
-
facingMode: videoConstraints.facingMode,
|
|
553
|
-
preferredCamera: this.preferredCamera,
|
|
554
|
-
deviceType: this.deviceType
|
|
555
|
-
});
|
|
556
464
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
465
|
+
// Optimization: Use cached capabilities if available
|
|
466
|
+
let capabilities;
|
|
467
|
+
if (this.selectedCameraId && CameraService.cameraCapabilitiesCache.has(this.selectedCameraId + '_caps')) {
|
|
468
|
+
capabilities = JSON.parse(CameraService.cameraCapabilitiesCache.get(this.selectedCameraId + '_caps'));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
const tempStream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
|
|
472
|
+
const videoTrack = tempStream.getVideoTracks()[0];
|
|
473
|
+
capabilities = videoTrack.getCapabilities();
|
|
474
|
+
tempStream.getTracks().forEach(track => track.stop());
|
|
475
|
+
// Cache capabilities for future use
|
|
476
|
+
if (this.selectedCameraId) {
|
|
477
|
+
CameraService.cameraCapabilitiesCache.set(this.selectedCameraId + '_caps', JSON.stringify(capabilities));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
563
480
|
const constraints = { ...videoConstraints };
|
|
564
|
-
// Enhanced focus and image quality settings
|
|
565
|
-
this.applyAdvancedCameraSettings(constraints, capabilities);
|
|
566
481
|
if (capabilities.width && capabilities.height) {
|
|
567
482
|
const maxWidth = Math.min(capabilities.width.max, 1920);
|
|
568
483
|
const maxHeight = Math.min(capabilities.height.max, 1080);
|
|
@@ -576,15 +491,20 @@ class CameraService {
|
|
|
576
491
|
constraints.height = { ideal: maxHeight };
|
|
577
492
|
}
|
|
578
493
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
494
|
+
// Configure autofocus if camera has the capability
|
|
495
|
+
const capabilitiesAny = capabilities;
|
|
496
|
+
if (capabilitiesAny.focusMode && Array.isArray(capabilitiesAny.focusMode)) {
|
|
497
|
+
if (capabilitiesAny.focusMode.includes('continuous')) {
|
|
498
|
+
constraints.focusMode = 'continuous';
|
|
499
|
+
}
|
|
500
|
+
else if (capabilitiesAny.focusMode.includes('auto')) {
|
|
501
|
+
constraints.focusMode = 'auto';
|
|
502
|
+
}
|
|
503
|
+
}
|
|
583
504
|
return constraints;
|
|
584
505
|
}
|
|
585
506
|
catch (err) {
|
|
586
|
-
|
|
587
|
-
// Ensure device type is detected before determining constraints
|
|
507
|
+
// Fallback logic with minimal temporary streams
|
|
588
508
|
if (!this.deviceType || this.deviceType === 'desktop') {
|
|
589
509
|
await this.detectDeviceType();
|
|
590
510
|
}
|
|
@@ -593,25 +513,16 @@ class CameraService {
|
|
|
593
513
|
width: { ideal: isTablet ? 1280 : 1920 },
|
|
594
514
|
height: { ideal: isTablet ? 720 : 1080 }
|
|
595
515
|
};
|
|
596
|
-
|
|
597
|
-
this.applyBasicFocusSettings(fallbackConstraints);
|
|
598
|
-
if (this.isManuallySelected && this.selectedCameraId) {
|
|
599
|
-
// When manually selected, use deviceId for exact camera selection
|
|
516
|
+
if (this.selectedCameraId) {
|
|
600
517
|
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
601
518
|
}
|
|
602
519
|
else if (this.preferredCameraFacing) {
|
|
603
|
-
// Use facingMode for initial preference-based selection
|
|
604
|
-
// Use 'ideal' instead of 'exact' to avoid OverconstrainedError on desktop
|
|
605
520
|
const facingConstraint = this.deviceType === 'desktop'
|
|
606
521
|
? { ideal: this.preferredCameraFacing }
|
|
607
522
|
: { exact: this.preferredCameraFacing };
|
|
608
523
|
fallbackConstraints.facingMode = facingConstraint;
|
|
609
524
|
}
|
|
610
|
-
else if (this.selectedCameraId) {
|
|
611
|
-
fallbackConstraints.deviceId = { exact: this.selectedCameraId };
|
|
612
|
-
}
|
|
613
525
|
else {
|
|
614
|
-
// Use preferredCamera setting to determine default facing mode
|
|
615
526
|
if (this.preferredCamera === 'front') {
|
|
616
527
|
fallbackConstraints.facingMode = "user";
|
|
617
528
|
}
|
|
@@ -619,165 +530,26 @@ class CameraService {
|
|
|
619
530
|
fallbackConstraints.facingMode = "environment";
|
|
620
531
|
}
|
|
621
532
|
else {
|
|
622
|
-
// Auto mode: use environment for mobile/tablet, user for desktop
|
|
623
533
|
fallbackConstraints.facingMode = (this.deviceType === 'mobile' || this.deviceType === 'tablet') ? "environment" : "user";
|
|
624
534
|
}
|
|
625
535
|
}
|
|
626
536
|
return fallbackConstraints;
|
|
627
537
|
}
|
|
628
538
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (capabilities.frameRate) {
|
|
633
|
-
constraints.frameRate = {
|
|
634
|
-
ideal: 30,
|
|
635
|
-
min: 15,
|
|
636
|
-
max: 60
|
|
637
|
-
};
|
|
638
|
-
}
|
|
639
|
-
// Width and height constraints for optimal resolution
|
|
640
|
-
if (capabilities.width && capabilities.height) {
|
|
641
|
-
constraints.width = {
|
|
642
|
-
ideal: Math.min(1920, capabilities.width.max || 1920),
|
|
643
|
-
min: 640
|
|
644
|
-
};
|
|
645
|
-
constraints.height = {
|
|
646
|
-
ideal: Math.min(1080, capabilities.height.max || 1080),
|
|
647
|
-
min: 480
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
// Apply enhanced constraints through advanced array for better browser compatibility
|
|
651
|
-
const advancedConstraints = [];
|
|
652
|
-
// Try to set optimal settings through advanced constraints
|
|
653
|
-
const advanced = {};
|
|
654
|
-
// Focus mode - use 'any' type to bypass TypeScript limitations
|
|
655
|
-
if (capabilities.focusMode) {
|
|
656
|
-
if (capabilities.focusMode.includes('continuous')) {
|
|
657
|
-
advanced.focusMode = 'continuous';
|
|
658
|
-
}
|
|
659
|
-
else if (capabilities.focusMode.includes('single-shot')) {
|
|
660
|
-
advanced.focusMode = 'single-shot';
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
// Focus distance for close objects
|
|
664
|
-
if (capabilities.focusDistance) {
|
|
665
|
-
advanced.focusDistance = {
|
|
666
|
-
ideal: 0.4, // 40cm - optimal for document capture
|
|
667
|
-
min: 0.2,
|
|
668
|
-
max: 1.0
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
// Zoom control
|
|
672
|
-
if (capabilities.zoom) {
|
|
673
|
-
advanced.zoom = {
|
|
674
|
-
ideal: 1.0,
|
|
675
|
-
min: capabilities.zoom.min,
|
|
676
|
-
max: Math.min(capabilities.zoom.max, 3.0)
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
// Add advanced constraints if any were set
|
|
680
|
-
if (Object.keys(advanced).length > 0) {
|
|
681
|
-
advancedConstraints.push(advanced);
|
|
682
|
-
}
|
|
683
|
-
if (advancedConstraints.length > 0) {
|
|
684
|
-
constraints.advanced = advancedConstraints;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
applyBasicFocusSettings(constraints) {
|
|
688
|
-
// Apply basic focus settings when capabilities are not available
|
|
689
|
-
// Use advanced constraints for non-standard properties
|
|
690
|
-
const advanced = {
|
|
691
|
-
focusMode: 'continuous',
|
|
692
|
-
exposureMode: 'continuous',
|
|
693
|
-
whiteBalanceMode: 'continuous'
|
|
694
|
-
};
|
|
695
|
-
if (!constraints.advanced) {
|
|
696
|
-
constraints.advanced = [];
|
|
697
|
-
}
|
|
698
|
-
constraints.advanced.push(advanced);
|
|
699
|
-
constraints.frameRate = { ideal: 30 };
|
|
700
|
-
}
|
|
701
|
-
getCapabilitiesSummary(capabilities) {
|
|
702
|
-
return {
|
|
703
|
-
hasAutoFocus: !!capabilities.focusMode,
|
|
704
|
-
hasFocusDistance: !!capabilities.focusDistance,
|
|
705
|
-
hasExposureControl: !!capabilities.exposureMode,
|
|
706
|
-
hasWhiteBalance: !!capabilities.whiteBalanceMode,
|
|
707
|
-
hasZoom: !!capabilities.zoom,
|
|
708
|
-
hasTorch: capabilities.torch !== undefined,
|
|
709
|
-
hasImageControls: !!(capabilities.brightness || capabilities.contrast || capabilities.saturation || capabilities.sharpness),
|
|
710
|
-
resolutionRange: capabilities.width && capabilities.height ?
|
|
711
|
-
`${capabilities.width.min}x${capabilities.height.min} - ${capabilities.width.max}x${capabilities.height.max}` : 'unknown'
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
// New method to enable torch/flash for better illumination
|
|
715
|
-
async setTorchEnabled(enabled, stream) {
|
|
716
|
-
try {
|
|
717
|
-
if (!stream) {
|
|
718
|
-
this.logger.warn('No hay stream disponible para controlar el flash');
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
const videoTrack = stream.getVideoTracks()[0];
|
|
722
|
-
if (!videoTrack) {
|
|
723
|
-
this.logger.warn('No hay track de video disponible');
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
const capabilities = videoTrack.getCapabilities();
|
|
727
|
-
if (capabilities.torch === undefined) {
|
|
728
|
-
this.logger.warn('El dispositivo no soporta control de flash');
|
|
729
|
-
return false;
|
|
730
|
-
}
|
|
731
|
-
await videoTrack.applyConstraints({
|
|
732
|
-
advanced: [{ torch: enabled }]
|
|
733
|
-
});
|
|
734
|
-
this.logger.state('FLASH_CONTROLADO', { enabled });
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
catch (error) {
|
|
738
|
-
this.logger.error('Error al controlar el flash:', error);
|
|
539
|
+
// Optimization helper methods
|
|
540
|
+
constraintsEqual(a, b) {
|
|
541
|
+
if (!a || !b)
|
|
739
542
|
return false;
|
|
740
|
-
|
|
543
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
741
544
|
}
|
|
742
|
-
//
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (!videoTrack) {
|
|
751
|
-
this.logger.warn('No hay track de video disponible');
|
|
752
|
-
return false;
|
|
753
|
-
}
|
|
754
|
-
const capabilities = videoTrack.getCapabilities();
|
|
755
|
-
if (!capabilities.focusMode || !capabilities.focusMode.includes('single-shot')) {
|
|
756
|
-
this.logger.warn('El dispositivo no soporta enfoque manual');
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
// Trigger single-shot focus
|
|
760
|
-
await videoTrack.applyConstraints({
|
|
761
|
-
advanced: [{ focusMode: 'single-shot' }]
|
|
762
|
-
});
|
|
763
|
-
// Return to continuous focus after a short delay
|
|
764
|
-
setTimeout(async () => {
|
|
765
|
-
try {
|
|
766
|
-
await videoTrack.applyConstraints({
|
|
767
|
-
advanced: [{ focusMode: 'continuous' }]
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
catch (error) {
|
|
771
|
-
this.logger.warn('Error al volver a enfoque continuo:', error);
|
|
772
|
-
}
|
|
773
|
-
}, 1000);
|
|
774
|
-
this.logger.state('ENFOQUE_MANUAL_ACTIVADO', { x, y });
|
|
775
|
-
return true;
|
|
776
|
-
}
|
|
777
|
-
catch (error) {
|
|
778
|
-
this.logger.error('Error al enfocar manualmente:', error);
|
|
779
|
-
return false;
|
|
780
|
-
}
|
|
545
|
+
// Cleanup method for optimization
|
|
546
|
+
static clearCaches() {
|
|
547
|
+
CameraService.cameraCapabilitiesCache.clear();
|
|
548
|
+
CameraService.deviceEnumerationCache = null;
|
|
549
|
+
}
|
|
550
|
+
// Method to invalidate device cache when needed
|
|
551
|
+
invalidateDeviceCache() {
|
|
552
|
+
CameraService.deviceEnumerationCache = null;
|
|
781
553
|
}
|
|
782
554
|
}
|
|
783
555
|
|
|
@@ -856,9 +628,9 @@ class DeviceStrategyFactory {
|
|
|
856
628
|
}
|
|
857
629
|
|
|
858
630
|
class DetectionService {
|
|
859
|
-
logger;
|
|
860
631
|
debug;
|
|
861
632
|
useDocumentClassification;
|
|
633
|
+
useDocumentDetector;
|
|
862
634
|
session;
|
|
863
635
|
mobileNetSession;
|
|
864
636
|
mobileNetClassMap;
|
|
@@ -873,30 +645,32 @@ class DetectionService {
|
|
|
873
645
|
preprocessCanvas;
|
|
874
646
|
preprocessCtx;
|
|
875
647
|
captureCanvas;
|
|
876
|
-
|
|
877
|
-
|
|
648
|
+
// Static canvas pool for reuse across instances
|
|
649
|
+
static canvasPool = new Map();
|
|
650
|
+
static MAX_POOL_SIZE = 5;
|
|
651
|
+
constructor(debug = false, useDocumentClassification = false, useDocumentDetector = true) {
|
|
878
652
|
this.debug = debug;
|
|
879
653
|
this.useDocumentClassification = useDocumentClassification;
|
|
654
|
+
this.useDocumentDetector = useDocumentDetector;
|
|
880
655
|
this.deviceStrategy = DeviceStrategyFactory.createStrategy();
|
|
881
656
|
this.initializeCanvasPool();
|
|
882
657
|
}
|
|
883
658
|
async loadModel() {
|
|
884
659
|
if (this.modelLoaded || this.session) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (!this.useDocumentDetector) {
|
|
663
|
+
// Skip loading the detection model but mark as "loaded" for compatibility
|
|
664
|
+
this.modelLoaded = true;
|
|
889
665
|
return;
|
|
890
666
|
}
|
|
891
667
|
try {
|
|
892
|
-
this.logger.state('PRECARGANDO_MODELO_DETECCION', { modelPath: this.MODEL_PATH });
|
|
893
668
|
const sessionOptions = this.deviceStrategy.getSessionOptions(this.debug);
|
|
894
669
|
try {
|
|
895
670
|
this.session = await window.ort.InferenceSession.create(this.MODEL_PATH, sessionOptions);
|
|
896
671
|
}
|
|
897
672
|
catch (error) {
|
|
898
673
|
if (error.message.includes('failed to allocate a buffer')) {
|
|
899
|
-
this.logger.warn('Fallo en asignación de buffer durante precarga, intentando con configuración mínima');
|
|
900
674
|
const fallbackOptions = {
|
|
901
675
|
executionProviders: ['wasm'],
|
|
902
676
|
graphOptimizationLevel: 'disabled',
|
|
@@ -914,10 +688,8 @@ class DetectionService {
|
|
|
914
688
|
}
|
|
915
689
|
}
|
|
916
690
|
this.modelLoaded = true;
|
|
917
|
-
this.logger.state('MODELO_DETECCION_CARGADO_EXITOSAMENTE', { sessionCreated: !!this.session });
|
|
918
691
|
}
|
|
919
692
|
catch (error) {
|
|
920
|
-
this.logger.error('Error al precargar modelo de detección:', error);
|
|
921
693
|
throw error;
|
|
922
694
|
}
|
|
923
695
|
}
|
|
@@ -926,19 +698,14 @@ class DetectionService {
|
|
|
926
698
|
return;
|
|
927
699
|
}
|
|
928
700
|
try {
|
|
929
|
-
this.logger.state('CARGANDO_MODELO_MOBILENET', { path: this.MOBILENET_MODEL_PATH });
|
|
930
701
|
// Try to load class map (optional - SqueezeNet may not need it for basic classification)
|
|
931
702
|
try {
|
|
932
703
|
const classResponse = await fetch(this.MOBILENET_CLASSES_PATH);
|
|
933
704
|
if (classResponse.ok) {
|
|
934
705
|
this.mobileNetClassMap = await classResponse.json();
|
|
935
|
-
this.logger.state('CLASES_MOBILENET_CARGADAS', {
|
|
936
|
-
classCount: Object.keys(this.mobileNetClassMap).length
|
|
937
|
-
});
|
|
938
706
|
}
|
|
939
707
|
}
|
|
940
708
|
catch (error) {
|
|
941
|
-
this.logger.warn('No se pudo cargar el mapa de clases de MobileNet, usando clasificación básica');
|
|
942
709
|
// Create a basic class map for document types
|
|
943
710
|
this.mobileNetClassMap = {
|
|
944
711
|
"0": "document",
|
|
@@ -955,7 +722,6 @@ class DetectionService {
|
|
|
955
722
|
}
|
|
956
723
|
catch (error) {
|
|
957
724
|
if (error.message.includes('failed to allocate a buffer')) {
|
|
958
|
-
this.logger.warn('Fallo en asignación de buffer de MobileNet, intentando con configuración mínima');
|
|
959
725
|
const fallbackOptions = {
|
|
960
726
|
executionProviders: ['wasm'],
|
|
961
727
|
graphOptimizationLevel: 'disabled',
|
|
@@ -972,16 +738,16 @@ class DetectionService {
|
|
|
972
738
|
throw error;
|
|
973
739
|
}
|
|
974
740
|
}
|
|
975
|
-
this.logger.state('MODELO_MOBILENET_CARGADO_EXITOSAMENTE', {
|
|
976
|
-
sessionCreated: !!this.mobileNetSession
|
|
977
|
-
});
|
|
978
741
|
}
|
|
979
742
|
catch (error) {
|
|
980
|
-
this.logger.error('Error al cargar modelo MobileNet:', error);
|
|
981
743
|
throw error;
|
|
982
744
|
}
|
|
983
745
|
}
|
|
984
746
|
isModelLoaded() {
|
|
747
|
+
if (!this.useDocumentDetector) {
|
|
748
|
+
// If detector is disabled, consider it "loaded" for compatibility
|
|
749
|
+
return this.modelLoaded;
|
|
750
|
+
}
|
|
985
751
|
return this.modelLoaded && !!this.session;
|
|
986
752
|
}
|
|
987
753
|
preprocess(video) {
|
|
@@ -1006,6 +772,10 @@ class DetectionService {
|
|
|
1006
772
|
return new window.ort.Tensor("float16", float16Data, [1, 3, this.INPUT_SIZE, this.INPUT_SIZE]);
|
|
1007
773
|
}
|
|
1008
774
|
async runInference(inputTensor) {
|
|
775
|
+
if (!this.useDocumentDetector) {
|
|
776
|
+
// Return empty array when document detector is disabled
|
|
777
|
+
return [];
|
|
778
|
+
}
|
|
1009
779
|
if (!this.session) {
|
|
1010
780
|
throw new Error('Detection model not loaded');
|
|
1011
781
|
}
|
|
@@ -1030,11 +800,9 @@ class DetectionService {
|
|
|
1030
800
|
}
|
|
1031
801
|
async classifyDocument(canvas) {
|
|
1032
802
|
if (!this.mobileNetSession || !this.mobileNetClassMap) {
|
|
1033
|
-
this.logger.warn('Modelo MobileNet no está cargado, saltando clasificación');
|
|
1034
803
|
return null;
|
|
1035
804
|
}
|
|
1036
805
|
try {
|
|
1037
|
-
this.logger.state('CLASIFICANDO_DOCUMENTO', { timestamp: Date.now() });
|
|
1038
806
|
const inputTensor = this.preprocessMobileNet(canvas);
|
|
1039
807
|
const feeds = { [this.mobileNetSession.inputNames[0]]: inputTensor };
|
|
1040
808
|
const results = await this.mobileNetSession.run(feeds);
|
|
@@ -1042,13 +810,6 @@ class DetectionService {
|
|
|
1042
810
|
const maxIdx = output.reduce((bestIdx, val, idx, arr) => val > arr[bestIdx] ? idx : bestIdx, 0);
|
|
1043
811
|
const confidence = output[maxIdx];
|
|
1044
812
|
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
813
|
return {
|
|
1053
814
|
class: className,
|
|
1054
815
|
confidence: confidence,
|
|
@@ -1056,7 +817,6 @@ class DetectionService {
|
|
|
1056
817
|
};
|
|
1057
818
|
}
|
|
1058
819
|
catch (error) {
|
|
1059
|
-
this.logger.error('Error al clasificar documento:', error);
|
|
1060
820
|
return null;
|
|
1061
821
|
}
|
|
1062
822
|
}
|
|
@@ -1169,7 +929,6 @@ class DetectionService {
|
|
|
1169
929
|
}
|
|
1170
930
|
this.mobileNetClassMap = undefined;
|
|
1171
931
|
this.modelLoaded = false;
|
|
1172
|
-
this.logger.state('DETECCION_SERVICE_LIMPIADO', { timestamp: Date.now() });
|
|
1173
932
|
}
|
|
1174
933
|
initializeCanvasPool() {
|
|
1175
934
|
this.preprocessCanvas = document.createElement('canvas');
|
|
@@ -1180,9 +939,6 @@ class DetectionService {
|
|
|
1180
939
|
willReadFrequently: true
|
|
1181
940
|
});
|
|
1182
941
|
this.captureCanvas = document.createElement('canvas');
|
|
1183
|
-
this.logger.state('CANVAS_POOL_INICIALIZADO', {
|
|
1184
|
-
preprocessCanvasSize: this.INPUT_SIZE
|
|
1185
|
-
});
|
|
1186
942
|
}
|
|
1187
943
|
cleanupCanvasPool() {
|
|
1188
944
|
if (this.preprocessCanvas) {
|
|
@@ -1218,10 +974,11 @@ class DetectionService {
|
|
|
1218
974
|
return (sign << 15) | (newExp << 10) | (frac >> 13);
|
|
1219
975
|
}
|
|
1220
976
|
preprocessMobileNet(canvas) {
|
|
1221
|
-
|
|
1222
|
-
tempCanvas
|
|
1223
|
-
tempCanvas.height = 224;
|
|
977
|
+
// Use pooled canvas instead of creating new one
|
|
978
|
+
const tempCanvas = this.getCanvas(224, 224);
|
|
1224
979
|
const tempCtx = tempCanvas.getContext('2d');
|
|
980
|
+
// Clear canvas before use
|
|
981
|
+
tempCtx.clearRect(0, 0, 224, 224);
|
|
1225
982
|
// Resize image to 224x224 for SqueezeNet (using MobileNet interface)
|
|
1226
983
|
tempCtx.drawImage(canvas, 0, 0, 224, 224);
|
|
1227
984
|
const imageData = tempCtx.getImageData(0, 0, 224, 224);
|
|
@@ -1237,8 +994,53 @@ class DetectionService {
|
|
|
1237
994
|
// Blue channel
|
|
1238
995
|
arr[2 * hw + i] = data[i * 4 + 2] / 255.0;
|
|
1239
996
|
}
|
|
997
|
+
// Return canvas to pool after use
|
|
998
|
+
this.returnCanvas(tempCanvas);
|
|
1240
999
|
return new window.ort.Tensor('float32', arr, [1, 3, 224, 224]);
|
|
1241
1000
|
}
|
|
1001
|
+
// Canvas pool management methods for optimization
|
|
1002
|
+
getCanvas(width, height) {
|
|
1003
|
+
const key = `${width}x${height}`;
|
|
1004
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1005
|
+
DetectionService.canvasPool.set(key, []);
|
|
1006
|
+
}
|
|
1007
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1008
|
+
let canvas = pool.pop();
|
|
1009
|
+
if (!canvas) {
|
|
1010
|
+
canvas = document.createElement('canvas');
|
|
1011
|
+
canvas.width = width;
|
|
1012
|
+
canvas.height = height;
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
// Clear the canvas before reuse
|
|
1016
|
+
const ctx = canvas.getContext('2d');
|
|
1017
|
+
ctx.clearRect(0, 0, width, height);
|
|
1018
|
+
}
|
|
1019
|
+
return canvas;
|
|
1020
|
+
}
|
|
1021
|
+
returnCanvas(canvas) {
|
|
1022
|
+
const key = `${canvas.width}x${canvas.height}`;
|
|
1023
|
+
if (!DetectionService.canvasPool.has(key)) {
|
|
1024
|
+
DetectionService.canvasPool.set(key, []);
|
|
1025
|
+
}
|
|
1026
|
+
const pool = DetectionService.canvasPool.get(key);
|
|
1027
|
+
// Only return to pool if not at max capacity
|
|
1028
|
+
if (pool.length < DetectionService.MAX_POOL_SIZE) {
|
|
1029
|
+
pool.push(canvas);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
// Static method to clear all canvas pools (useful for memory management)
|
|
1033
|
+
static clearCanvasPools() {
|
|
1034
|
+
DetectionService.canvasPool.clear();
|
|
1035
|
+
}
|
|
1036
|
+
// Method to get pool statistics for debugging
|
|
1037
|
+
getPoolStats() {
|
|
1038
|
+
const stats = {};
|
|
1039
|
+
DetectionService.canvasPool.forEach((pool, key) => {
|
|
1040
|
+
stats[key] = pool.length;
|
|
1041
|
+
});
|
|
1042
|
+
return stats;
|
|
1043
|
+
}
|
|
1242
1044
|
}
|
|
1243
1045
|
|
|
1244
1046
|
class ServiceContainer {
|
|
@@ -1249,13 +1051,11 @@ class ServiceContainer {
|
|
|
1249
1051
|
initializeServices(config) {
|
|
1250
1052
|
// Initialize services in dependency order
|
|
1251
1053
|
const eventBus = new EventBusService();
|
|
1252
|
-
const logger = new LoggerService(config.debug);
|
|
1253
1054
|
const stateManager = new StateManagerService(eventBus);
|
|
1254
|
-
const cameraService = new CameraService(
|
|
1255
|
-
const detectionService = new DetectionService(
|
|
1055
|
+
const cameraService = new CameraService(eventBus, config.preferredCamera);
|
|
1056
|
+
const detectionService = new DetectionService(config.debug, config.useDocumentClassification, config.useDocumentDetector);
|
|
1256
1057
|
// Register services
|
|
1257
1058
|
this.services.set('eventBus', eventBus);
|
|
1258
|
-
this.services.set('logger', logger);
|
|
1259
1059
|
this.services.set('stateManager', stateManager);
|
|
1260
1060
|
this.services.set('cameraService', cameraService);
|
|
1261
1061
|
this.services.set('detectionService', detectionService);
|
|
@@ -1267,9 +1067,6 @@ class ServiceContainer {
|
|
|
1267
1067
|
}
|
|
1268
1068
|
return service;
|
|
1269
1069
|
}
|
|
1270
|
-
getLogger() {
|
|
1271
|
-
return this.get('logger');
|
|
1272
|
-
}
|
|
1273
1070
|
getEventBus() {
|
|
1274
1071
|
return this.get('eventBus');
|
|
1275
1072
|
}
|
|
@@ -1283,10 +1080,6 @@ class ServiceContainer {
|
|
|
1283
1080
|
return this.get('detectionService');
|
|
1284
1081
|
}
|
|
1285
1082
|
updateConfig(config) {
|
|
1286
|
-
if (config.debug !== undefined) {
|
|
1287
|
-
const logger = this.services.get('logger');
|
|
1288
|
-
logger.setDebugMode(config.debug);
|
|
1289
|
-
}
|
|
1290
1083
|
}
|
|
1291
1084
|
cleanup() {
|
|
1292
1085
|
// Cleanup services in reverse order
|
|
@@ -1296,7 +1089,7 @@ class ServiceContainer {
|
|
|
1296
1089
|
}
|
|
1297
1090
|
}
|
|
1298
1091
|
|
|
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}}";
|
|
1092
|
+
const myComponentCss = ":host{display:block;width:100%;height:100%;font-family:system-ui, -apple-system, sans-serif;color:#1a1a1a}.detector-container{display:flex;flex-direction:column;align-items:center;width:100%;height:100%}h1{font-size:24px;font-weight:500;color:#333;margin:0 0 24px 0}.video-container{position:relative;width:100%;height:100%;background:#333;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden}video,.detection-overlay{position:absolute;width:100%;height:100%;border-radius:8px}video.mirror,.detection-overlay.mirror{transform:rotateY(180deg)}.detection-overlay{z-index:1;pointer-events:none}video{z-index:0}.detection-box{transition:opacity 0.2s ease}.status{margin-top:16px;font-size:12px;color:#666}.overlay-mask{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10;display:block;pointer-events:none}.card-outline{position:absolute;top:var(--mask-center-y, 50%);left:var(--mask-center-x, 50%);transform:translate(-50%, -50%);width:var(--mask-width, 88%);height:var(--mask-height, 55%);border:none;border-radius:4px;background:transparent;opacity:0.8}.side{position:absolute;background:#999;transition:background-color 0.3s ease;border-radius:1px}.side-top.aligned{border-left-color:#28a745;border-top-color:#28a745}.side-right.aligned{border-right-color:#28a745;border-top-color:#28a745}.side-bottom.aligned{border-left-color:#28a745;border-bottom-color:#28a745}.side-left.aligned{border-right-color:#28a745;border-bottom-color:#28a745}.side-top{top:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-right{top:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-top:6px solid #ffffff;background:transparent}.side-bottom{bottom:-3px;left:-3px;width:30px;height:30px;border-left:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.side-left{bottom:-3px;right:-3px;width:30px;height:30px;border-right:6px solid #ffffff;border-bottom:6px solid #ffffff;background:transparent}.guide-text{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:rgba(0, 0, 0, 0.25);backdrop-filter:blur(20px);border-radius:12px;border:1px solid rgba(255, 255, 255, 0.1);color:#ffffff;font-size:12px;font-weight:500;text-align:center;white-space:normal;padding:13px;max-width:300px;z-index:20;height:fit-content;width:fit-content;animation:slideInFromTopCentered 0.3s ease-out}.quality-indicator{position:absolute;top:20px;right:20px;display:flex;align-items:center;gap:8px;background:rgba(0, 0, 0, 0.8);padding:8px 12px;border-radius:20px;z-index:25;transition:all 0.3s ease}.quality-indicator.warning{background:rgba(255, 152, 0, 0.9)}.quality-indicator.good{background:rgba(76, 175, 80, 0.9)}.quality-score{color:#fff;font-size:12px;font-weight:600;min-width:30px;text-align:center}.quality-status{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;color:#fff;transition:all 0.3s ease}.quality-status.ready{background:#4CAF50}.quality-status.not-ready{background:#f44336}.card-outline.quality-warning{animation:qualityWarning 1s ease-in-out infinite alternate}@keyframes qualityWarning{0%{box-shadow:0 0 0 3px rgba(255, 152, 0, 0.6)}100%{box-shadow:0 0 0 6px rgba(255, 152, 0, 0.3)}}.capture-animation{position:absolute;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;z-index:30;pointer-events:none;animation:captureFlash 0.6s ease-out}@keyframes captureFlash{0%{opacity:0}15%{opacity:0.8}30%{opacity:0}45%{opacity:0.4}60%{opacity:0}100%{opacity:0}}.card-outline.capturing{animation:pulseGreen 0.6s ease-out}@keyframes pulseGreen{0%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}50%{border-color:#28a745;box-shadow:0 0 0 8px rgba(40, 167, 69, 0.6)}100%{border-color:#28a745;box-shadow:0 0 0 4px rgba(40, 167, 69, 0)}}.flip-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showFlipInstruction 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.id-card-icon{width:80px;height:50px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:8px;position:relative;animation:flipCard 2s ease-in-out infinite;box-shadow:0 4px 12px rgba(0, 0, 0, 0.3)}.id-card-icon::before{content:'';position:absolute;top:8px;left:8px;width:16px;height:12px;background:rgba(255, 255, 255, 0.9);border-radius:2px}.id-card-icon::after{content:'';position:absolute;top:25px;left:8px;width:64px;height:3px;background:rgba(255, 255, 255, 0.7);border-radius:1px;box-shadow:0 6px 0 rgba(255, 255, 255, 0.7), 0 12px 0 rgba(255, 255, 255, 0.7)}.flip-text{margin-top:12px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px}@keyframes showFlipInstruction{0%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.8)}}@keyframes flipCard{0%{transform:rotateY(0deg)}50%{transform:rotateY(180deg)}100%{transform:rotateY(360deg)}}.success-animation{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);z-index:35;pointer-events:none;opacity:0;animation:showSuccessMessage 3s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center}.check-icon{width:80px;height:80px;background:linear-gradient(135deg, #6c757d 0%, #495057 100%);border-radius:50%;position:relative;animation:bounceSuccess 0.6s ease-out;box-shadow:0 4px 16px rgba(108, 117, 125, 0.4);display:flex;align-items:center;justify-content:center}.check-icon::before{content:'';position:absolute;width:20px;height:35px;border:4px solid #fff;border-top:none;border-left:none;transform:rotate(45deg);animation:drawCheck 0.4s ease-out 0.2s both}.success-text{margin-top:16px;color:#fff;font-size:14px;font-weight:600;text-align:center;background:rgba(128, 128, 128, 0.8);padding:12px 20px;border-radius:20px;max-width:300px;animation:fadeInUp 0.5s ease-out 0.4s both}@keyframes showSuccessMessage{0%{opacity:0;transform:translate(-50%, -50%) scale(0.5)}15%{opacity:1;transform:translate(-50%, -50%) scale(1)}85%{opacity:1;transform:translate(-50%, -50%) scale(1)}100%{opacity:0;transform:translate(-50%, -50%) scale(0.9)}}@keyframes bounceSuccess{0%{transform:scale(0)}50%{transform:scale(1.1)}100%{transform:scale(1)}}@keyframes drawCheck{0%{width:0;height:0}50%{width:20px;height:0}100%{width:20px;height:35px}}@keyframes fadeInUp{0%{opacity:0;transform:translateY(20px)}100%{opacity:1;transform:translateY(0)}}.skip-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:300px}.back-capture-section{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);z-index:25;display:flex;flex-direction:column;align-items:center;width:100%;max-width:320px}.back-capture-buttons{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap}.capture-button{pointer-events:auto;background:#fff;color:#333;border:none;border-radius:25px;padding:12px 24px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.3s ease}.capture-button:hover{background:#f8f9fa}.capture-button:active{background:#e9ecef;transform:translateY(1px)}.capture-button:disabled{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@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}}.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;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(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)}}@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{background:#e9ecef;color:#6c757d;cursor:not-allowed;transform:none}@media (max-width: 480px){.manual-capture-section{bottom:16px;max-width:280px}.manual-capture-button{padding:10px 20px;font-size:13px}}";
|
|
1300
1093
|
|
|
1301
1094
|
const JaakStamps = class {
|
|
1302
1095
|
constructor(hostRef) {
|
|
@@ -1310,6 +1103,7 @@ const JaakStamps = class {
|
|
|
1310
1103
|
maskSize = 90;
|
|
1311
1104
|
cropMargin = 20;
|
|
1312
1105
|
useDocumentClassification = false;
|
|
1106
|
+
useDocumentDetector = true;
|
|
1313
1107
|
preferredCamera = 'auto';
|
|
1314
1108
|
captureDelay = 1500;
|
|
1315
1109
|
enableBackDocumentTimer = false;
|
|
@@ -1326,9 +1120,16 @@ const JaakStamps = class {
|
|
|
1326
1120
|
showCameraSelector = false;
|
|
1327
1121
|
isSwitchingCamera = false;
|
|
1328
1122
|
hasDocumentDetected = false;
|
|
1123
|
+
cameraInfoWithAutofocus = {
|
|
1124
|
+
availableCameras: [],
|
|
1125
|
+
selectedCameraId: null,
|
|
1126
|
+
deviceType: 'desktop',
|
|
1127
|
+
isMultipleCamerasAvailable: false,
|
|
1128
|
+
preferredFacing: null
|
|
1129
|
+
};
|
|
1329
1130
|
currentStatus = {
|
|
1330
|
-
message: '
|
|
1331
|
-
description: 'Configurando
|
|
1131
|
+
message: 'Preparando capturador...',
|
|
1132
|
+
description: 'Configurando herramientas de captura',
|
|
1332
1133
|
type: 'initializing',
|
|
1333
1134
|
isInitialized: false
|
|
1334
1135
|
};
|
|
@@ -1343,9 +1144,9 @@ const JaakStamps = class {
|
|
|
1343
1144
|
detectionRate: 0
|
|
1344
1145
|
};
|
|
1345
1146
|
backDocumentTimerRemaining = 0;
|
|
1147
|
+
showManualCaptureButton = false;
|
|
1346
1148
|
// Services
|
|
1347
1149
|
serviceContainer;
|
|
1348
|
-
logger;
|
|
1349
1150
|
eventBus;
|
|
1350
1151
|
stateManager;
|
|
1351
1152
|
cameraService;
|
|
@@ -1376,19 +1177,25 @@ const JaakStamps = class {
|
|
|
1376
1177
|
lastUpdateTime: 0
|
|
1377
1178
|
};
|
|
1378
1179
|
performanceUpdateInterval;
|
|
1379
|
-
// Performance optimization
|
|
1180
|
+
// Performance optimization with adaptive frame skipping
|
|
1380
1181
|
frameSkipCounter = 0;
|
|
1381
|
-
|
|
1182
|
+
BASE_FRAME_SKIP = 2;
|
|
1183
|
+
MAX_FRAME_SKIP = 8;
|
|
1382
1184
|
consecutiveFailures = 0;
|
|
1383
1185
|
MAX_FAILURES = 30;
|
|
1384
1186
|
lastInferenceTime = 0;
|
|
1385
1187
|
MIN_INFERENCE_INTERVAL = 50;
|
|
1188
|
+
performanceHistory = [];
|
|
1189
|
+
PERFORMANCE_HISTORY_SIZE = 10;
|
|
1190
|
+
// Canvas pool for optimized screenshot capture
|
|
1191
|
+
canvasPool = [];
|
|
1192
|
+
MAX_CANVAS_POOL_SIZE = 3;
|
|
1386
1193
|
async componentDidLoad() {
|
|
1387
|
-
this.updateStatus('
|
|
1194
|
+
this.updateStatus('Preparando capturador...', 'Configurando herramientas de captura', 'initializing');
|
|
1388
1195
|
await this.initializeServices();
|
|
1389
|
-
this.updateStatus('
|
|
1196
|
+
this.updateStatus('Preparando funciones...', 'Configurando herramientas de trabajo', 'initializing');
|
|
1390
1197
|
await this.setupEventListeners();
|
|
1391
|
-
this.updateStatus('
|
|
1198
|
+
this.updateStatus('Configurando cámara...', 'Detectando dispositivos disponibles', 'initializing');
|
|
1392
1199
|
await this.initializeComponent();
|
|
1393
1200
|
if (this.debug) {
|
|
1394
1201
|
this.initializePerformanceMonitor();
|
|
@@ -1401,37 +1208,28 @@ const JaakStamps = class {
|
|
|
1401
1208
|
maskSize: this.maskSize,
|
|
1402
1209
|
cropMargin: this.cropMargin,
|
|
1403
1210
|
useDocumentClassification: this.useDocumentClassification,
|
|
1211
|
+
useDocumentDetector: this.useDocumentDetector,
|
|
1404
1212
|
preferredCamera: this.preferredCamera,
|
|
1405
1213
|
captureDelay: this.captureDelay
|
|
1406
1214
|
};
|
|
1407
1215
|
this.serviceContainer = new ServiceContainer(config);
|
|
1408
|
-
this.logger = this.serviceContainer.getLogger();
|
|
1409
1216
|
this.eventBus = this.serviceContainer.getEventBus();
|
|
1410
1217
|
this.stateManager = this.serviceContainer.getStateManager();
|
|
1411
1218
|
this.cameraService = this.serviceContainer.getCameraService();
|
|
1412
1219
|
this.detectionService = this.serviceContainer.getDetectionService();
|
|
1413
|
-
this.logger.state('SERVICIOS_INICIALIZADOS', { timestamp: Date.now() });
|
|
1414
1220
|
}
|
|
1415
1221
|
async setupEventListeners() {
|
|
1416
1222
|
this.eventBus.on('state-changed', (data) => {
|
|
1417
1223
|
this.handleStateChange(data);
|
|
1418
1224
|
});
|
|
1419
|
-
this.eventBus.on('camera-changed', (
|
|
1420
|
-
|
|
1225
|
+
this.eventBus.on('camera-changed', () => {
|
|
1226
|
+
// Camera changed event
|
|
1421
1227
|
});
|
|
1422
|
-
this.eventBus.on('error', (
|
|
1423
|
-
|
|
1228
|
+
this.eventBus.on('error', () => {
|
|
1229
|
+
// Handle service errors
|
|
1424
1230
|
});
|
|
1425
1231
|
}
|
|
1426
1232
|
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
1233
|
this.validateProps();
|
|
1436
1234
|
if (this.debug) {
|
|
1437
1235
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
@@ -1440,58 +1238,67 @@ const JaakStamps = class {
|
|
|
1440
1238
|
this.updateStatus('Detectando cámaras...', 'Buscando dispositivos de captura', 'initializing');
|
|
1441
1239
|
await this.cameraService.detectDeviceType();
|
|
1442
1240
|
await this.cameraService.enumerateDevices();
|
|
1443
|
-
this.
|
|
1241
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1242
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'initializing');
|
|
1444
1243
|
await this.loadOnnxRuntime();
|
|
1445
1244
|
this.initializeResizeObserver();
|
|
1446
1245
|
}
|
|
1447
1246
|
validateProps() {
|
|
1448
1247
|
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
1248
|
this.maskSize = 90;
|
|
1451
1249
|
}
|
|
1452
1250
|
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
1251
|
this.cropMargin = 0;
|
|
1455
1252
|
}
|
|
1456
1253
|
const validOptions = ['auto', 'front', 'back'];
|
|
1457
1254
|
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
1255
|
this.preferredCamera = 'auto';
|
|
1460
1256
|
}
|
|
1461
1257
|
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
1258
|
this.captureDelay = 1500;
|
|
1464
1259
|
}
|
|
1465
1260
|
}
|
|
1466
1261
|
async loadOnnxRuntime() {
|
|
1467
1262
|
if (!window.ort) {
|
|
1468
|
-
this.updateStatus('
|
|
1263
|
+
this.updateStatus('Preparando herramientas...', 'Configurando funciones de captura', 'initializing');
|
|
1469
1264
|
const script = document.createElement('script');
|
|
1470
1265
|
script.src = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js';
|
|
1471
1266
|
document.head.appendChild(script);
|
|
1472
1267
|
await new Promise((resolve) => {
|
|
1473
1268
|
script.onload = () => {
|
|
1474
|
-
setTimeout(() => {
|
|
1475
|
-
this.finalizeInitialization();
|
|
1269
|
+
setTimeout(async () => {
|
|
1270
|
+
await this.finalizeInitialization();
|
|
1476
1271
|
resolve(undefined);
|
|
1477
1272
|
}, 300);
|
|
1478
1273
|
};
|
|
1479
1274
|
});
|
|
1480
1275
|
}
|
|
1481
1276
|
else {
|
|
1482
|
-
setTimeout(() => {
|
|
1483
|
-
this.finalizeInitialization();
|
|
1277
|
+
setTimeout(async () => {
|
|
1278
|
+
await this.finalizeInitialization();
|
|
1484
1279
|
}, 300);
|
|
1485
1280
|
}
|
|
1486
1281
|
}
|
|
1487
|
-
finalizeInitialization() {
|
|
1282
|
+
async finalizeInitialization() {
|
|
1488
1283
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1284
|
+
// Check camera permissions before showing "ready" status
|
|
1285
|
+
const hasPermissions = await this.checkCameraPermissions();
|
|
1286
|
+
if (hasPermissions) {
|
|
1287
|
+
this.currentStatus = {
|
|
1288
|
+
message: 'Listo para capturar',
|
|
1289
|
+
description: '',
|
|
1290
|
+
type: 'ready',
|
|
1291
|
+
isInitialized: true
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
this.currentStatus = {
|
|
1296
|
+
message: 'Permisos de cámara requeridos',
|
|
1297
|
+
description: 'Por favor, otorgue permisos de cámara para continuar',
|
|
1298
|
+
type: 'error',
|
|
1299
|
+
isInitialized: true
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1495
1302
|
this.emitReadyEvent();
|
|
1496
1303
|
}
|
|
1497
1304
|
updateStatus(message, description, type = 'loading') {
|
|
@@ -1505,11 +1312,26 @@ const JaakStamps = class {
|
|
|
1505
1312
|
emitReadyEvent() {
|
|
1506
1313
|
const isDocumentReady = !!window.ort && this.detectionService.isModelLoaded();
|
|
1507
1314
|
this.isReady.emit(isDocumentReady);
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1315
|
+
}
|
|
1316
|
+
async checkCameraPermissions() {
|
|
1317
|
+
try {
|
|
1318
|
+
if (!navigator.permissions) {
|
|
1319
|
+
// Fallback: try to access camera directly
|
|
1320
|
+
try {
|
|
1321
|
+
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
|
1322
|
+
stream.getTracks().forEach(track => track.stop());
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const permission = await navigator.permissions.query({ name: 'camera' });
|
|
1330
|
+
return permission.state === 'granted';
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
return false;
|
|
1334
|
+
}
|
|
1513
1335
|
}
|
|
1514
1336
|
handleStateChange(data) {
|
|
1515
1337
|
}
|
|
@@ -1526,9 +1348,31 @@ const JaakStamps = class {
|
|
|
1526
1348
|
const container = this.detectionContainer.parentElement;
|
|
1527
1349
|
const rect = container.getBoundingClientRect();
|
|
1528
1350
|
this.updateMaskDimensions(rect);
|
|
1529
|
-
this.logger.debug('Container redimensionado:', { width: rect.width, height: rect.height });
|
|
1530
1351
|
}
|
|
1531
1352
|
}
|
|
1353
|
+
getGuideText(step) {
|
|
1354
|
+
if (step === 'completed') {
|
|
1355
|
+
return 'Proceso completado';
|
|
1356
|
+
}
|
|
1357
|
+
if (step === 'front') {
|
|
1358
|
+
// Para el frente, si el detector manual está activo, mostrar instrucciones de captura manual
|
|
1359
|
+
if (this.showManualCaptureButton) {
|
|
1360
|
+
return 'Posicione el frente de su documento en el marco y presione el botón para capturar';
|
|
1361
|
+
}
|
|
1362
|
+
// Instrucción básica de alineación
|
|
1363
|
+
return 'Alinee su identificación con el marco';
|
|
1364
|
+
}
|
|
1365
|
+
else if (step === 'back') {
|
|
1366
|
+
// Para el reverso, mostrar instrucciones específicas según el modo
|
|
1367
|
+
if (this.useDocumentDetector) {
|
|
1368
|
+
return 'Si tu documento no tiene lado trasero, da clic en el botón para continuar con el proceso';
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
return 'Posicione el reverso de su documento en el marco y capture, o salte este paso';
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return 'Alinee su identificación con el marco';
|
|
1375
|
+
}
|
|
1532
1376
|
updateMaskDimensions(containerRect) {
|
|
1533
1377
|
if (!this.videoRef)
|
|
1534
1378
|
return;
|
|
@@ -1580,16 +1424,38 @@ const JaakStamps = class {
|
|
|
1580
1424
|
this.el.style.setProperty('--mask-center-x', `${videoCenterXPercent}%`);
|
|
1581
1425
|
this.el.style.setProperty('--mask-center-y', `${videoCenterYPercent}%`);
|
|
1582
1426
|
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
1427
|
}
|
|
1591
1428
|
// PUBLIC METHODS
|
|
1429
|
+
// Helper method to check if component is ready for operations
|
|
1430
|
+
isComponentReady() {
|
|
1431
|
+
if (!this.currentStatus.isInitialized) {
|
|
1432
|
+
return {
|
|
1433
|
+
ready: false,
|
|
1434
|
+
message: 'El componente aún se está inicializando',
|
|
1435
|
+
status: 'initializing'
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') {
|
|
1439
|
+
return {
|
|
1440
|
+
ready: false,
|
|
1441
|
+
message: 'El componente está cargando recursos',
|
|
1442
|
+
status: 'loading'
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
if (!this.serviceContainer || !this.stateManager || !this.cameraService || !this.detectionService) {
|
|
1446
|
+
return {
|
|
1447
|
+
ready: false,
|
|
1448
|
+
message: 'Los servicios del componente no están disponibles',
|
|
1449
|
+
status: 'error'
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
return { ready: true };
|
|
1453
|
+
}
|
|
1592
1454
|
async getCapturedImages() {
|
|
1455
|
+
const readyCheck = this.isComponentReady();
|
|
1456
|
+
if (!readyCheck.ready) {
|
|
1457
|
+
throw new Error(readyCheck.message);
|
|
1458
|
+
}
|
|
1593
1459
|
const state = this.stateManager.getCaptureState();
|
|
1594
1460
|
if (state.step !== 'completed') {
|
|
1595
1461
|
throw new Error('El proceso de captura no ha sido completado');
|
|
@@ -1597,47 +1463,128 @@ const JaakStamps = class {
|
|
|
1597
1463
|
return this.stateManager.getCapturedImages();
|
|
1598
1464
|
}
|
|
1599
1465
|
async isProcessCompleted() {
|
|
1466
|
+
const readyCheck = this.isComponentReady();
|
|
1467
|
+
if (!readyCheck.ready) {
|
|
1468
|
+
return false; // Return false instead of error for this method
|
|
1469
|
+
}
|
|
1600
1470
|
return this.stateManager.isProcessCompleted();
|
|
1601
1471
|
}
|
|
1602
1472
|
async startCapture() {
|
|
1603
|
-
|
|
1473
|
+
const readyCheck = this.isComponentReady();
|
|
1474
|
+
if (!readyCheck.ready) {
|
|
1475
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1476
|
+
return { success: false, error: readyCheck.message };
|
|
1477
|
+
}
|
|
1478
|
+
try {
|
|
1479
|
+
await this.startDetection();
|
|
1480
|
+
return { success: true };
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
this.updateStatus('Error al iniciar captura', error.message, 'error');
|
|
1484
|
+
return { success: false, error: error.message };
|
|
1485
|
+
}
|
|
1604
1486
|
}
|
|
1605
1487
|
async stopCapture() {
|
|
1606
|
-
this.
|
|
1488
|
+
const readyCheck = this.isComponentReady();
|
|
1489
|
+
if (!readyCheck.ready) {
|
|
1490
|
+
return { success: false, error: readyCheck.message };
|
|
1491
|
+
}
|
|
1492
|
+
try {
|
|
1493
|
+
this.exitSession();
|
|
1494
|
+
return { success: true };
|
|
1495
|
+
}
|
|
1496
|
+
catch (error) {
|
|
1497
|
+
return { success: false, error: error.message };
|
|
1498
|
+
}
|
|
1607
1499
|
}
|
|
1608
1500
|
async resetCapture() {
|
|
1609
|
-
this.
|
|
1501
|
+
const readyCheck = this.isComponentReady();
|
|
1502
|
+
if (!readyCheck.ready) {
|
|
1503
|
+
return { success: false, error: readyCheck.message };
|
|
1504
|
+
}
|
|
1505
|
+
try {
|
|
1506
|
+
this.resetDetection();
|
|
1507
|
+
return { success: true };
|
|
1508
|
+
}
|
|
1509
|
+
catch (error) {
|
|
1510
|
+
return { success: false, error: error.message };
|
|
1511
|
+
}
|
|
1610
1512
|
}
|
|
1611
1513
|
async skipBackCapture() {
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1514
|
+
const readyCheck = this.isComponentReady();
|
|
1515
|
+
if (!readyCheck.ready) {
|
|
1516
|
+
return { success: false, error: readyCheck.message };
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1520
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1521
|
+
if (captureState.step === 'back' && capturedImages.front.fullFrame) {
|
|
1522
|
+
this.completeProcess(true);
|
|
1523
|
+
return { success: true };
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
return { success: false, error: 'No se puede saltar el reverso en el estado actual' };
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
catch (error) {
|
|
1530
|
+
return { success: false, error: error.message };
|
|
1616
1531
|
}
|
|
1617
1532
|
}
|
|
1618
1533
|
async getStatus() {
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1534
|
+
const readyCheck = this.isComponentReady();
|
|
1535
|
+
if (!readyCheck.ready) {
|
|
1536
|
+
return {
|
|
1537
|
+
isVideoActive: false,
|
|
1538
|
+
captureStep: 'front',
|
|
1539
|
+
hasImages: false,
|
|
1540
|
+
isProcessCompleted: false,
|
|
1541
|
+
isModelPreloaded: false,
|
|
1542
|
+
componentStatus: readyCheck.status,
|
|
1543
|
+
componentMessage: readyCheck.message
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1548
|
+
const capturedImages = this.stateManager.getCapturedImages();
|
|
1549
|
+
return {
|
|
1550
|
+
isVideoActive: captureState.isVideoActive,
|
|
1551
|
+
captureStep: captureState.step,
|
|
1552
|
+
hasImages: !!(capturedImages.front.fullFrame || capturedImages.back.fullFrame),
|
|
1553
|
+
isProcessCompleted: this.stateManager.isProcessCompleted(),
|
|
1554
|
+
isModelPreloaded: this.detectionService.isModelLoaded(),
|
|
1555
|
+
componentStatus: 'ready'
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
catch (error) {
|
|
1559
|
+
return {
|
|
1560
|
+
isVideoActive: false,
|
|
1561
|
+
captureStep: 'front',
|
|
1562
|
+
hasImages: false,
|
|
1563
|
+
isProcessCompleted: false,
|
|
1564
|
+
isModelPreloaded: false,
|
|
1565
|
+
componentStatus: 'error',
|
|
1566
|
+
componentMessage: error.message
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1628
1569
|
}
|
|
1629
1570
|
async preloadModel() {
|
|
1571
|
+
const readyCheck = this.isComponentReady();
|
|
1572
|
+
if (!readyCheck.ready) {
|
|
1573
|
+
this.updateStatus(readyCheck.message, 'Espere a que termine la inicialización', readyCheck.status);
|
|
1574
|
+
return { success: false, error: readyCheck.message };
|
|
1575
|
+
}
|
|
1630
1576
|
if (this.detectionService.isModelLoaded()) {
|
|
1631
|
-
this.
|
|
1632
|
-
this.updateStatus('Modelos ya cargados', '', 'ready');
|
|
1577
|
+
this.updateStatus('Reconocimiento listo', '', 'ready');
|
|
1633
1578
|
return { success: true, message: 'Model already loaded' };
|
|
1634
1579
|
}
|
|
1635
1580
|
try {
|
|
1636
1581
|
const loadStartTime = performance.now();
|
|
1637
|
-
this.updateStatus('
|
|
1638
|
-
this.stateManager
|
|
1582
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1583
|
+
if (this.stateManager) {
|
|
1584
|
+
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1585
|
+
}
|
|
1639
1586
|
await this.detectionService.loadModel();
|
|
1640
|
-
this.updateStatus('
|
|
1587
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1641
1588
|
await this.detectionService.loadClassificationModel();
|
|
1642
1589
|
const loadEndTime = performance.now();
|
|
1643
1590
|
const loadTime = loadEndTime - loadStartTime;
|
|
@@ -1645,80 +1592,118 @@ const JaakStamps = class {
|
|
|
1645
1592
|
if (this.debug) {
|
|
1646
1593
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1647
1594
|
}
|
|
1648
|
-
this.updateStatus('
|
|
1595
|
+
this.updateStatus('Finalizando configuración...', 'Preparando herramientas para captura', 'loading');
|
|
1649
1596
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
1650
|
-
this.updateStatus('
|
|
1651
|
-
this.stateManager
|
|
1597
|
+
this.updateStatus('Reconocimiento preparado', '', 'ready');
|
|
1598
|
+
if (this.stateManager) {
|
|
1599
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1600
|
+
}
|
|
1652
1601
|
this.emitReadyEvent();
|
|
1653
|
-
this.logger.state('MODELOS_PRECARGADOS_EXITOSAMENTE', { loadTime: Math.round(loadTime) });
|
|
1654
1602
|
return { success: true, message: 'Models preloaded successfully' };
|
|
1655
1603
|
}
|
|
1656
1604
|
catch (error) {
|
|
1657
|
-
this.
|
|
1658
|
-
this.
|
|
1659
|
-
|
|
1605
|
+
this.updateStatus('Error de configuración', 'No se pudieron preparar las herramientas necesarias', 'error');
|
|
1606
|
+
if (this.stateManager) {
|
|
1607
|
+
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1608
|
+
}
|
|
1660
1609
|
return { success: false, error: error.message };
|
|
1661
1610
|
}
|
|
1662
1611
|
}
|
|
1663
1612
|
async getCameraInfo() {
|
|
1664
|
-
|
|
1613
|
+
const readyCheck = this.isComponentReady();
|
|
1614
|
+
if (!readyCheck.ready) {
|
|
1615
|
+
return {
|
|
1616
|
+
availableCameras: [],
|
|
1617
|
+
selectedCameraId: null,
|
|
1618
|
+
deviceType: 'desktop',
|
|
1619
|
+
isMultipleCamerasAvailable: false,
|
|
1620
|
+
preferredFacing: null,
|
|
1621
|
+
error: readyCheck.message
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
try {
|
|
1625
|
+
return await this.cameraService.getCameraInfo();
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
return {
|
|
1629
|
+
availableCameras: [],
|
|
1630
|
+
selectedCameraId: null,
|
|
1631
|
+
deviceType: 'desktop',
|
|
1632
|
+
isMultipleCamerasAvailable: false,
|
|
1633
|
+
preferredFacing: null,
|
|
1634
|
+
error: error.message
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1665
1637
|
}
|
|
1666
1638
|
async setPreferredCamera(camera) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1639
|
+
const readyCheck = this.isComponentReady();
|
|
1640
|
+
if (!readyCheck.ready) {
|
|
1641
|
+
return {
|
|
1642
|
+
success: false,
|
|
1643
|
+
error: readyCheck.message,
|
|
1644
|
+
selectedCamera: null,
|
|
1645
|
+
availableCameras: 0
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
try {
|
|
1649
|
+
this.preferredCamera = camera;
|
|
1650
|
+
this.serviceContainer.updateConfig({ preferredCamera: camera });
|
|
1651
|
+
await this.cameraService.enumerateDevices();
|
|
1652
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1653
|
+
const captureState = this.stateManager.getCaptureState();
|
|
1654
|
+
if (captureState.isVideoActive) {
|
|
1655
|
+
const selectedCameraId = this.cameraService.getSelectedCameraId();
|
|
1656
|
+
if (selectedCameraId) {
|
|
1657
|
+
await this.cameraService.switchCamera(selectedCameraId);
|
|
1658
|
+
}
|
|
1675
1659
|
}
|
|
1660
|
+
return {
|
|
1661
|
+
success: true,
|
|
1662
|
+
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1663
|
+
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
return {
|
|
1668
|
+
success: false,
|
|
1669
|
+
error: error.message,
|
|
1670
|
+
selectedCamera: null,
|
|
1671
|
+
availableCameras: 0
|
|
1672
|
+
};
|
|
1676
1673
|
}
|
|
1677
|
-
return {
|
|
1678
|
-
success: true,
|
|
1679
|
-
selectedCamera: this.cameraService.getSelectedCameraId(),
|
|
1680
|
-
availableCameras: this.cameraService.getAvailableCameras().length
|
|
1681
|
-
};
|
|
1682
1674
|
}
|
|
1683
1675
|
async setCaptureDelay(delay) {
|
|
1684
1676
|
if (delay < 0 || delay > 10000) {
|
|
1685
|
-
|
|
1677
|
+
return {
|
|
1678
|
+
success: false,
|
|
1679
|
+
error: 'Capture delay must be between 0 and 10000 milliseconds',
|
|
1680
|
+
captureDelay: this.captureDelay
|
|
1681
|
+
};
|
|
1686
1682
|
}
|
|
1683
|
+
// Allow setting capture delay even during initialization
|
|
1687
1684
|
this.captureDelay = delay;
|
|
1688
|
-
this.serviceContainer
|
|
1685
|
+
if (this.serviceContainer) {
|
|
1686
|
+
this.serviceContainer.updateConfig({ captureDelay: delay });
|
|
1687
|
+
}
|
|
1689
1688
|
return {
|
|
1690
1689
|
success: true,
|
|
1691
1690
|
captureDelay: this.captureDelay
|
|
1692
1691
|
};
|
|
1693
1692
|
}
|
|
1694
1693
|
async getCaptureDelay() {
|
|
1694
|
+
// Always allow getting capture delay, even during initialization
|
|
1695
1695
|
return this.captureDelay;
|
|
1696
1696
|
}
|
|
1697
|
-
async setTorchEnabled(enabled) {
|
|
1698
|
-
const torchEnabled = await this.cameraService.setTorchEnabled(enabled, this.videoStream);
|
|
1699
|
-
return {
|
|
1700
|
-
success: torchEnabled,
|
|
1701
|
-
enabled: torchEnabled ? enabled : false
|
|
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 }
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
1697
|
// DETECTION METHODS
|
|
1712
1698
|
async startDetection() {
|
|
1713
|
-
this.logger.state('INICIANDO_DETECCION');
|
|
1714
1699
|
try {
|
|
1715
1700
|
// Paso 1: Verificar y cargar modelos si es necesario
|
|
1716
1701
|
if (!this.detectionService.isModelLoaded()) {
|
|
1717
1702
|
const loadStartTime = performance.now();
|
|
1718
|
-
this.updateStatus('
|
|
1703
|
+
this.updateStatus('Preparando reconocimiento...', 'Configurando detección de documentos', 'loading');
|
|
1719
1704
|
this.stateManager.updateCaptureState({ isLoading: true });
|
|
1720
1705
|
await this.detectionService.loadModel();
|
|
1721
|
-
this.updateStatus('
|
|
1706
|
+
this.updateStatus('Optimizando detección...', 'Configurando reconocimiento de documentos', 'loading');
|
|
1722
1707
|
await this.detectionService.loadClassificationModel();
|
|
1723
1708
|
const loadEndTime = performance.now();
|
|
1724
1709
|
const loadTime = loadEndTime - loadStartTime;
|
|
@@ -1726,16 +1711,21 @@ const JaakStamps = class {
|
|
|
1726
1711
|
if (this.debug) {
|
|
1727
1712
|
this.recordOnnxPerformance(loadTime, 0);
|
|
1728
1713
|
}
|
|
1729
|
-
this.logger.state('MODELOS_CARGADOS_EN_DETECCION', { loadTime: Math.round(loadTime) });
|
|
1730
1714
|
}
|
|
1731
1715
|
// Paso 2: Detectar y configurar dispositivos
|
|
1732
|
-
this.updateStatus('
|
|
1716
|
+
this.updateStatus('Configurando cámara...', 'Buscando dispositivos disponibles', 'loading');
|
|
1733
1717
|
await this.cameraService.enumerateDevices();
|
|
1718
|
+
await this.updateCameraInfoWithAutofocus();
|
|
1734
1719
|
// Paso 3: Configurar cámara seleccionada
|
|
1735
|
-
this.updateStatus('
|
|
1720
|
+
this.updateStatus('Ajustando cámara...', 'Configurando calidad de imagen', 'loading');
|
|
1736
1721
|
const stream = await this.cameraService.setupCamera();
|
|
1737
1722
|
// Paso 4: Inicializar video y ocultar estado inmediatamente
|
|
1738
|
-
this.
|
|
1723
|
+
if (this.useDocumentDetector) {
|
|
1724
|
+
this.updateStatus('Captura activa', 'Buscando documento en el marco de captura', 'active');
|
|
1725
|
+
}
|
|
1726
|
+
else {
|
|
1727
|
+
this.updateStatus('Captura activa', 'Posicione su documento y use el botón para capturar', 'active');
|
|
1728
|
+
}
|
|
1739
1729
|
await this.initializeVideoStream(stream);
|
|
1740
1730
|
// Paso 5: Calibrar máscara
|
|
1741
1731
|
if (this.detectionContainer) {
|
|
@@ -1746,11 +1736,14 @@ const JaakStamps = class {
|
|
|
1746
1736
|
// Finalizar e iniciar captura
|
|
1747
1737
|
this.startTime = Date.now();
|
|
1748
1738
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1739
|
+
// Mostrar botón manual si el detector está deshabilitado
|
|
1740
|
+
if (!this.useDocumentDetector) {
|
|
1741
|
+
this.showManualCaptureButton = true;
|
|
1742
|
+
}
|
|
1749
1743
|
this.detectFrame();
|
|
1750
1744
|
}
|
|
1751
1745
|
catch (err) {
|
|
1752
|
-
this.
|
|
1753
|
-
this.updateStatus('Error al iniciar captura', 'No se pudo completar la inicialización', 'error');
|
|
1746
|
+
this.updateStatus('Error al iniciar', 'No se pudo preparar la captura', 'error');
|
|
1754
1747
|
this.stateManager.updateCaptureState({ isLoading: false });
|
|
1755
1748
|
}
|
|
1756
1749
|
}
|
|
@@ -1760,10 +1753,6 @@ const JaakStamps = class {
|
|
|
1760
1753
|
this.videoStream = stream;
|
|
1761
1754
|
const isRear = this.cameraService.isRearCamera(stream);
|
|
1762
1755
|
this.shouldMirrorVideo = !isRear;
|
|
1763
|
-
this.logger.state('CAMARA_CONFIGURADA', {
|
|
1764
|
-
isRearCamera: isRear,
|
|
1765
|
-
shouldMirrorVideo: this.shouldMirrorVideo
|
|
1766
|
-
});
|
|
1767
1756
|
return new Promise((resolve) => {
|
|
1768
1757
|
this.videoRef.onloadedmetadata = async () => {
|
|
1769
1758
|
await this.videoRef.play();
|
|
@@ -1782,15 +1771,28 @@ const JaakStamps = class {
|
|
|
1782
1771
|
const captureState = this.stateManager.getCaptureState();
|
|
1783
1772
|
if (!this.videoRef || !this.detectionContainer || !this.detectionService.isModelLoaded())
|
|
1784
1773
|
return;
|
|
1774
|
+
// Show manual capture button when document detector is disabled
|
|
1775
|
+
if (!this.useDocumentDetector) {
|
|
1776
|
+
this.showManualCaptureButton = true;
|
|
1777
|
+
// Continue the loop for UI updates but skip detection logic
|
|
1778
|
+
if (captureState.step !== 'completed') {
|
|
1779
|
+
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1780
|
+
}
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
else {
|
|
1784
|
+
this.showManualCaptureButton = false;
|
|
1785
|
+
}
|
|
1785
1786
|
if (captureState.isDetectionPaused) {
|
|
1786
1787
|
if (captureState.step !== 'completed') {
|
|
1787
1788
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1788
1789
|
}
|
|
1789
1790
|
return;
|
|
1790
1791
|
}
|
|
1791
|
-
//
|
|
1792
|
+
// Adaptive frame skipping for performance
|
|
1792
1793
|
this.frameSkipCounter++;
|
|
1793
|
-
|
|
1794
|
+
const currentFrameSkip = this.getAdaptiveFrameSkip();
|
|
1795
|
+
if (this.frameSkipCounter <= currentFrameSkip) {
|
|
1794
1796
|
if (captureState.step !== 'completed') {
|
|
1795
1797
|
this.animationId = requestAnimationFrame(() => this.detectFrame());
|
|
1796
1798
|
}
|
|
@@ -1866,7 +1868,6 @@ const JaakStamps = class {
|
|
|
1866
1868
|
}
|
|
1867
1869
|
}
|
|
1868
1870
|
catch (e) {
|
|
1869
|
-
this.logger.error('Error en inferencia de modelo:', e);
|
|
1870
1871
|
const captureState = this.stateManager.getCaptureState();
|
|
1871
1872
|
if (captureState.step !== 'completed') {
|
|
1872
1873
|
setTimeout(() => this.detectFrame(), 100);
|
|
@@ -1893,6 +1894,8 @@ const JaakStamps = class {
|
|
|
1893
1894
|
clearInterval(this.performanceUpdateInterval);
|
|
1894
1895
|
this.performanceUpdateInterval = undefined;
|
|
1895
1896
|
}
|
|
1897
|
+
// Clear canvas pool
|
|
1898
|
+
this.canvasPool = [];
|
|
1896
1899
|
this.detectionBoxes = [];
|
|
1897
1900
|
this.alignmentStartTime = undefined;
|
|
1898
1901
|
this.hasDocumentDetected = false;
|
|
@@ -1906,11 +1909,8 @@ const JaakStamps = class {
|
|
|
1906
1909
|
step: 'front',
|
|
1907
1910
|
isCapturing: false
|
|
1908
1911
|
};
|
|
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: {
|
|
1912
|
+
const cameraInfo = this.cameraInfoWithAutofocus;
|
|
1913
|
+
return (h("div", { key: 'e0b32b1b41b58e3a58b552199384080e8f2c069b', class: "detector-container" }, h("div", { key: '03c77af311ff12d936994d629d58550cbaae190c', class: "video-container" }, h("video", { key: '94f15b34f34367a10d17816a31cd147adb57bc90', ref: el => this.videoRef = el, autoplay: true, muted: true, playsinline: true, class: this.shouldMirrorVideo ? 'mirror' : '', style: { display: captureState.isVideoActive ? 'block' : 'none' } }), h("div", { key: '982be192792dfaf644eb173bfee9b26ebd229288', 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
1914
|
position: 'absolute',
|
|
1915
1915
|
left: `${box.x}px`,
|
|
1916
1916
|
top: `${box.y}px`,
|
|
@@ -1919,9 +1919,9 @@ const JaakStamps = class {
|
|
|
1919
1919
|
border: '2px solid #32406C',
|
|
1920
1920
|
pointerEvents: 'none',
|
|
1921
1921
|
boxSizing: 'border-box'
|
|
1922
|
-
} })))), this.isMaskReady && (h("div", { key: '
|
|
1922
|
+
} })))), this.isMaskReady && (h("div", { key: 'f5212ff70db65d2b836a3ac456a1701701630660', class: "overlay-mask" }, h("div", { key: '5080217f4962c2c7152def39cc978ba1c40a3a1c', class: "card-outline" }, h("div", { key: 'd37f1ca8723d79b10d1c6a3afaf54e2f2657ae79', class: "side side-top" }), h("div", { key: 'c5bb4f7d646a991f43598688ffa909c04697bb06', class: "side side-right" }), h("div", { key: '4a5040b4a236367dcdf72f4e72d497b8b6dee6c0', class: "side side-bottom" }), h("div", { key: 'c74ecef56eee8e92cd9491d693fa89a6c01bdbbd', class: "side side-left" })), !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '2a2932b0c03835193ebf86ddb5f13173b1dba367', class: "guide-text" }, this.getGuideText(captureState.step))), captureState.step === 'back' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: '41fcf2d79106d3ed928fc2842cb6a6725ba4d4f6', class: "back-capture-section" }, h("div", { key: '98dbe1bbe454bad42c3e155247f25a9151aaf1f9', class: "back-capture-buttons" }, !this.useDocumentDetector && (h("button", { key: '01693bc0d6915a4d02f318268020dfbcf3a70b94', class: "capture-button primary-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Reverso')), h("button", { key: '0a4ba55cbbea867b936a83a16a622a3c36a225f9', class: "skip-button", onClick: () => this.skipBackCapture(), type: "button" }, this.enableBackDocumentTimer && this.backDocumentTimerRemaining > 0
|
|
1923
1923
|
? `Saltar reverso (${this.backDocumentTimerRemaining}s)`
|
|
1924
|
-
: 'Saltar reverso'))), captureState.isVideoActive && (h("div", { key: '
|
|
1924
|
+
: 'Saltar reverso')))), captureState.isVideoActive && (h("div", { key: '1db208a53dea37211bdd88254e575142b7abf91f', class: "camera-controls" }, h("button", { key: '90755839358f997c3ce2f4c36a8d17d39ea5f8ad', 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: 'ef7889ab82f9eaabcabec3e3dc3ebb7850818dd4', class: "camera-selector-dropdown" }, h("div", { key: 'e28fb005316a742318be3cf8582f520392b71bba', class: "camera-selector-header" }, h("span", { key: '97338030b1aedb50c5f5a588ea03c0f19bcb0ddd' }, "Seleccionar C\u00E1mara"), h("button", { key: '1537c907dfab17c232a256f9fc9210cbd0125cc6', class: "close-selector", onClick: () => this.toggleCameraSelector(), type: "button" }, "\u00D7")), h("div", { key: '509178119e6eed5fe0a6ea287904a53bd9fbecbc', 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: '485f7e1a97c43ed280a106bde3c6ee910625025c', class: "device-info" }, h("small", { key: 'd76edba943f4e06c14024923496555fa1fe6d845' }, "Dispositivo: ", cameraInfo.deviceType)))), this.showManualCaptureButton && captureState.step === 'front' && !captureState.showFlipAnimation && !captureState.showSuccessAnimation && (h("div", { key: 'c5184dd93aa41cff61b88ea2046b481efc87c1f2', class: "manual-capture-section" }, h("button", { key: '6e45d9a0fec60469eb926ff353cb1a440d59f5fe', class: "manual-capture-button", onClick: () => this.takeManualScreenshot(), type: "button", disabled: captureState.isCapturing }, captureState.isCapturing ? 'Capturando...' : 'Capturar Frente'))))), captureState.isCapturing && (h("div", { key: '9e296a942ba51cc1fd152e76ae406620c84ed721', class: "capture-animation" })), captureState.showFlipAnimation && (h("div", { key: '517db50f7273788d94d5e6db6c40f65e00e18784', class: "flip-animation" }, h("div", { key: 'fbf6a26c4cc0539bd16b45899d19984af3449610', class: "id-card-icon" }), h("div", { key: 'fb3a201864d04531a078074458aa6e7f6580d565', class: "flip-text" }, "\u00A1Voltea tu identificaci\u00F3n!"))), captureState.showSuccessAnimation && (h("div", { key: '10c694e233d26523d538ff0d2edbf6dcfb8e37c8', class: "success-animation" }, h("div", { key: '87e545650825ab805e887667281ecdf8bdb34c45', class: "check-icon" }), h("div", { key: '359da71f391d68e9437b749b949ee90685b96b3f', class: "success-text" }, "\u00A1Proceso completado!"))), h("div", { key: '7c5533bb875eb3d9cdcc6cfdfc20e4d5bd98d103', class: `component-status status-${this.currentStatus.type}` }, (this.currentStatus.type === 'loading' || this.currentStatus.type === 'initializing') && (h("div", { key: 'f32df46ab83279387de606eadf53cd4a4009f449', class: "status-spinner" })), h("div", { key: 'dd640de5a3b64f5f99363318e2fc70e32ccbc7db', class: "status-content" }, h("div", { key: '618c0ab9cb0a24b0af3f6946052beab0d660fe6d', class: "status-message" }, this.currentStatus.message), this.currentStatus.description && (h("div", { key: 'e52c2a4945c7f5a190a446ac588aa1668c21f26d', class: "status-description" }, this.currentStatus.description)))), this.debug && (h("div", { key: '61cea10bc3438fb74aa176d71c4238b070cb0f09', class: "performance-monitor" }, h("div", { key: '44f6b1accb56d99a236b22c0769f870638d9c768', class: "performance-expanded" }, h("div", { key: '6febc85e319752b0aebc0a4e4050ea6d597375a7', class: "metrics-row" }, h("div", { key: '96b81895ad3cd742c96308ed53a2126af76eb619', class: "metric-compact" }, h("span", { key: '774a11c6fed6a87db7673b018b3cdc378c5252ef', class: "metric-label" }, "FPS"), h("span", { key: 'cb866dcbea661e5f715b82c4dd760f786fd5bb2a', class: `metric-value ${this.performanceData.fps < 15 ? 'warning' : this.performanceData.fps < 10 ? 'danger' : 'good'}` }, this.performanceData.fps)), h("div", { key: 'a935aaadb83ea400bfaa984d1d845bdfd87becc8', class: "metric-compact" }, h("span", { key: 'ffacaa2cb7838403a297145b70586e255cc845c7', class: "metric-label" }, "MEM"), h("span", { key: 'a1d7a7abec97dc0544d50b11b372f699f750b76f', class: `metric-value ${this.performanceData.memoryUsage > 100 ? 'warning' : this.performanceData.memoryUsage > 200 ? 'danger' : 'good'}` }, this.performanceData.memoryUsage, "MB"))), h("div", { key: 'bb3d156a46569a8c290bc518c42ec7957bc5b94e', class: "metrics-row" }, h("div", { key: '4cadd8f845e4c12120ed549e4a73e02c6cbd9257', class: "metric-compact" }, h("span", { key: '1fc5661222f495b79fd7eb126e04f52eecab6fb3', class: "metric-label" }, "INF"), h("span", { key: 'eca1f59d1c41ab99158eec81592bc7150bfeabe1', class: `metric-value ${this.performanceData.inferenceTime > 100 ? 'warning' : this.performanceData.inferenceTime > 200 ? 'danger' : 'good'}` }, this.performanceData.inferenceTime, "ms")), h("div", { key: 'f708e2757adcb03e633111d96bd6786fb6b80ea6', class: "metric-compact" }, h("span", { key: 'a13099ded64180996d6f65bf215483ba83daccbc', class: "metric-label" }, "FRAME"), h("span", { key: '2765cd1f7c1a49df9de64639cc1b777c79f89f4c', class: `metric-value ${this.performanceData.frameProcessingTime > 50 ? 'warning' : this.performanceData.frameProcessingTime > 100 ? 'danger' : 'good'}` }, this.performanceData.frameProcessingTime, "ms"))), h("div", { key: '13aaf85a1b4b66294c4204f02d956b64665aa766', class: "metrics-row" }, h("div", { key: '40012f3f646d70c535a606ebbe581a2fe6fd2e50', class: "metric-compact" }, h("span", { key: '03f8b17746fe404485820d06a50173678db2fd25', class: "metric-label" }, "DET"), h("span", { key: 'c4f87430f142355054d9361330d6a5d508974b70', class: "metric-value good" }, this.performanceData.successfulDetections, "/", this.performanceData.totalDetections)), h("div", { key: 'cf366a41d7f7cda6046c32cc7c514eb7b46b05d1', class: "metric-compact" }, h("span", { key: '925e7de15b96ba4dbbcdbf8a9f0e3173bae49573', class: "metric-label" }, "RATE"), h("span", { key: '9701e60c7793f2c205f3c9ffd254b6a115b8a68c', class: `metric-value ${this.performanceData.detectionRate < 30 ? 'danger' : this.performanceData.detectionRate < 60 ? 'warning' : 'good'}` }, this.performanceData.detectionRate, "%")))))), h("div", { key: '636aeb9b2b8dd992d8a28a8db44b213e12514096', class: "watermark" }, h("img", { key: '0407b550d962a6113e05ad75e25a1a1bbd76d9ce', src: "https://storage.googleapis.com/jaak-static/commons/powered-by-jaak.png", alt: "Powered by Jaak" })))));
|
|
1925
1925
|
}
|
|
1926
1926
|
// Utility methods
|
|
1927
1927
|
updateDetectionBoxes(boxes) {
|
|
@@ -2002,35 +2002,18 @@ const JaakStamps = class {
|
|
|
2002
2002
|
if (allSidesAligned && bestBox) {
|
|
2003
2003
|
cardOutline?.classList.add('perfect-match');
|
|
2004
2004
|
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
2005
|
if (!this.hasScreenshotTaken) {
|
|
2013
2006
|
const currentTime = Date.now();
|
|
2014
2007
|
// Initialize alignment start time if not set
|
|
2015
2008
|
if (!this.alignmentStartTime) {
|
|
2016
2009
|
this.alignmentStartTime = currentTime;
|
|
2017
|
-
this.logger.state('ALIGNMENT_TIMER_STARTED', { startTime: currentTime });
|
|
2018
2010
|
}
|
|
2019
2011
|
// Check if document has been aligned for the configured delay
|
|
2020
2012
|
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
2013
|
if (alignmentDuration >= this.captureDelay) {
|
|
2027
|
-
this.logger.state('TRIGGERING_CAPTURE', {
|
|
2028
|
-
alignmentDuration,
|
|
2029
|
-
captureDelay: this.captureDelay
|
|
2030
|
-
});
|
|
2031
2014
|
this.lastDetectedBox = bestBox;
|
|
2032
|
-
this.takeScreenshot().catch(
|
|
2033
|
-
|
|
2015
|
+
this.takeScreenshot().catch(() => {
|
|
2016
|
+
// Handle screenshot error silently
|
|
2034
2017
|
});
|
|
2035
2018
|
this.hasScreenshotTaken = true;
|
|
2036
2019
|
this.alignmentStartTime = undefined;
|
|
@@ -2053,24 +2036,122 @@ const JaakStamps = class {
|
|
|
2053
2036
|
}
|
|
2054
2037
|
}
|
|
2055
2038
|
}
|
|
2039
|
+
async takeManualScreenshot() {
|
|
2040
|
+
if (!this.videoRef)
|
|
2041
|
+
return;
|
|
2042
|
+
// When using manual capture, use mask coordinates for cropping
|
|
2043
|
+
await this.takeScreenshotWithMaskCoordinates();
|
|
2044
|
+
}
|
|
2045
|
+
async takeScreenshotWithMaskCoordinates() {
|
|
2046
|
+
if (!this.videoRef || !this.detectionContainer)
|
|
2047
|
+
return;
|
|
2048
|
+
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2049
|
+
this.triggerCaptureAnimation();
|
|
2050
|
+
// Use pooled canvas for optimization
|
|
2051
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2052
|
+
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2053
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2054
|
+
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2055
|
+
// Calculate mask coordinates for cropping
|
|
2056
|
+
const container = this.detectionContainer.parentElement;
|
|
2057
|
+
const containerRect = container.getBoundingClientRect();
|
|
2058
|
+
// Get mask dimensions from CSS properties
|
|
2059
|
+
const maskWidthPercent = parseFloat(this.el.style.getPropertyValue('--mask-width').replace('%', '')) || 90;
|
|
2060
|
+
const maskHeightPercent = parseFloat(this.el.style.getPropertyValue('--mask-height').replace('%', '')) || 56.25;
|
|
2061
|
+
const maskCenterXPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-x').replace('%', '')) || 50;
|
|
2062
|
+
const maskCenterYPercent = parseFloat(this.el.style.getPropertyValue('--mask-center-y').replace('%', '')) || 50;
|
|
2063
|
+
// Convert percentages to actual video coordinates
|
|
2064
|
+
const videoWidth = this.videoRef.videoWidth;
|
|
2065
|
+
const videoHeight = this.videoRef.videoHeight;
|
|
2066
|
+
const videoAspectRatio = videoWidth / videoHeight;
|
|
2067
|
+
const containerAspectRatio = containerRect.width / containerRect.height;
|
|
2068
|
+
let displayedVideoWidth, displayedVideoHeight;
|
|
2069
|
+
let videoOffsetX = 0, videoOffsetY = 0;
|
|
2070
|
+
if (videoAspectRatio > containerAspectRatio) {
|
|
2071
|
+
displayedVideoWidth = containerRect.width;
|
|
2072
|
+
displayedVideoHeight = containerRect.width / videoAspectRatio;
|
|
2073
|
+
videoOffsetY = (containerRect.height - displayedVideoHeight) / 2;
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
displayedVideoHeight = containerRect.height;
|
|
2077
|
+
displayedVideoWidth = containerRect.height * videoAspectRatio;
|
|
2078
|
+
videoOffsetX = (containerRect.width - displayedVideoWidth) / 2;
|
|
2079
|
+
}
|
|
2080
|
+
// Calculate mask coordinates in video space
|
|
2081
|
+
const maskWidthInContainer = (maskWidthPercent / 100) * containerRect.width;
|
|
2082
|
+
const maskHeightInContainer = (maskHeightPercent / 100) * containerRect.height;
|
|
2083
|
+
const maskCenterXInContainer = (maskCenterXPercent / 100) * containerRect.width;
|
|
2084
|
+
const maskCenterYInContainer = (maskCenterYPercent / 100) * containerRect.height;
|
|
2085
|
+
// Convert to video coordinates
|
|
2086
|
+
const scaleX = videoWidth / displayedVideoWidth;
|
|
2087
|
+
const scaleY = videoHeight / displayedVideoHeight;
|
|
2088
|
+
const maskCenterXInVideo = (maskCenterXInContainer - videoOffsetX) * scaleX;
|
|
2089
|
+
const maskCenterYInVideo = (maskCenterYInContainer - videoOffsetY) * scaleY;
|
|
2090
|
+
const maskWidthInVideo = maskWidthInContainer * scaleX;
|
|
2091
|
+
const maskHeightInVideo = maskHeightInContainer * scaleY;
|
|
2092
|
+
// Calculate crop coordinates
|
|
2093
|
+
const cropX = Math.max(0, maskCenterXInVideo - (maskWidthInVideo / 2) - this.cropMargin);
|
|
2094
|
+
const cropY = Math.max(0, maskCenterYInVideo - (maskHeightInVideo / 2) - this.cropMargin);
|
|
2095
|
+
const cropWidth = Math.min(maskWidthInVideo + (2 * this.cropMargin), videoWidth - cropX);
|
|
2096
|
+
const cropHeight = Math.min(maskHeightInVideo + (2 * this.cropMargin), videoHeight - cropY);
|
|
2097
|
+
// Use pooled canvas for cropped version
|
|
2098
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2099
|
+
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2100
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2101
|
+
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2102
|
+
const captureState = this.stateManager.getCaptureState();
|
|
2103
|
+
if (captureState.step === 'front') {
|
|
2104
|
+
this.stateManager.setCapturedImages({
|
|
2105
|
+
front: {
|
|
2106
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2107
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2108
|
+
}
|
|
2109
|
+
});
|
|
2110
|
+
// Check if document classification is enabled (independent of detector)
|
|
2111
|
+
if (this.useDocumentClassification) {
|
|
2112
|
+
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2113
|
+
if (classification && classification.class === 'passport') {
|
|
2114
|
+
this.completeProcess(true);
|
|
2115
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2116
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
this.stateManager.updateCaptureState({
|
|
2121
|
+
step: 'back',
|
|
2122
|
+
isDetectionPaused: true,
|
|
2123
|
+
showFlipAnimation: true
|
|
2124
|
+
});
|
|
2125
|
+
setTimeout(() => {
|
|
2126
|
+
this.stateManager.updateCaptureState({
|
|
2127
|
+
showFlipAnimation: false,
|
|
2128
|
+
isDetectionPaused: false
|
|
2129
|
+
});
|
|
2130
|
+
this.startBackDocumentTimer();
|
|
2131
|
+
}, 3000);
|
|
2132
|
+
}
|
|
2133
|
+
else if (captureState.step === 'back') {
|
|
2134
|
+
this.stateManager.setCapturedImages({
|
|
2135
|
+
back: {
|
|
2136
|
+
fullFrame: captureCanvas.toDataURL('image/png'),
|
|
2137
|
+
cropped: croppedCanvas.toDataURL('image/png')
|
|
2138
|
+
}
|
|
2139
|
+
});
|
|
2140
|
+
this.completeProcess(false);
|
|
2141
|
+
}
|
|
2142
|
+
// Return canvases to pool after use
|
|
2143
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2144
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2145
|
+
}
|
|
2056
2146
|
async takeScreenshot() {
|
|
2057
2147
|
if (!this.videoRef || !this.lastDetectedBox)
|
|
2058
2148
|
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
2149
|
this.stateManager.updateCaptureState({ isCapturing: true });
|
|
2068
2150
|
this.triggerCaptureAnimation();
|
|
2069
|
-
//
|
|
2070
|
-
const captureCanvas =
|
|
2071
|
-
captureCanvas.width = this.videoRef.videoWidth;
|
|
2072
|
-
captureCanvas.height = this.videoRef.videoHeight;
|
|
2151
|
+
// Use pooled canvas for optimization
|
|
2152
|
+
const captureCanvas = this.getPooledCanvas(this.videoRef.videoWidth, this.videoRef.videoHeight);
|
|
2073
2153
|
const captureCtx = captureCanvas.getContext('2d', { alpha: false });
|
|
2154
|
+
captureCtx.clearRect(0, 0, captureCanvas.width, captureCanvas.height);
|
|
2074
2155
|
captureCtx.drawImage(this.videoRef, 0, 0, captureCanvas.width, captureCanvas.height);
|
|
2075
2156
|
// Calculate crop coordinates
|
|
2076
2157
|
const INPUT_SIZE = 320;
|
|
@@ -2080,11 +2161,10 @@ const JaakStamps = class {
|
|
|
2080
2161
|
const cropY = Math.max(0, (this.lastDetectedBox.y * scaleY) - this.cropMargin);
|
|
2081
2162
|
const cropWidth = Math.min((this.lastDetectedBox.w * scaleX) + (2 * this.cropMargin), this.videoRef.videoWidth - cropX);
|
|
2082
2163
|
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;
|
|
2164
|
+
// Use pooled canvas for cropped version
|
|
2165
|
+
const croppedCanvas = this.getPooledCanvas(cropWidth, cropHeight);
|
|
2087
2166
|
const croppedCtx = croppedCanvas.getContext('2d', { alpha: false });
|
|
2167
|
+
croppedCtx.clearRect(0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
2088
2168
|
croppedCtx.drawImage(this.videoRef, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
2089
2169
|
const captureState = this.stateManager.getCaptureState();
|
|
2090
2170
|
if (captureState.step === 'front') {
|
|
@@ -2098,7 +2178,6 @@ const JaakStamps = class {
|
|
|
2098
2178
|
if (this.useDocumentClassification) {
|
|
2099
2179
|
const classification = await this.detectionService.classifyDocument(croppedCanvas);
|
|
2100
2180
|
if (classification && classification.class === 'passport') {
|
|
2101
|
-
this.logger.state('PASAPORTE_DETECTADO_SALTANDO_REVERSO', { classification: classification?.class });
|
|
2102
2181
|
this.completeProcess(true);
|
|
2103
2182
|
return;
|
|
2104
2183
|
}
|
|
@@ -2125,6 +2204,9 @@ const JaakStamps = class {
|
|
|
2125
2204
|
});
|
|
2126
2205
|
this.completeProcess(false);
|
|
2127
2206
|
}
|
|
2207
|
+
// Return canvases to pool after use
|
|
2208
|
+
this.returnCanvasToPool(captureCanvas);
|
|
2209
|
+
this.returnCanvasToPool(croppedCanvas);
|
|
2128
2210
|
}
|
|
2129
2211
|
triggerCaptureAnimation() {
|
|
2130
2212
|
const cardOutline = this.el.shadowRoot?.querySelector('.card-outline');
|
|
@@ -2153,11 +2235,6 @@ const JaakStamps = class {
|
|
|
2153
2235
|
setTimeout(() => {
|
|
2154
2236
|
this.stateManager.updateCaptureState({ showSuccessAnimation: false });
|
|
2155
2237
|
}, 3000);
|
|
2156
|
-
this.logger.state('PROCESO_COMPLETADO', {
|
|
2157
|
-
skippedBack,
|
|
2158
|
-
totalImages: capturedImages.metadata.totalImages,
|
|
2159
|
-
timestamp: new Date().toISOString()
|
|
2160
|
-
});
|
|
2161
2238
|
}
|
|
2162
2239
|
stopDetection() {
|
|
2163
2240
|
if (this.animationId) {
|
|
@@ -2166,7 +2243,6 @@ const JaakStamps = class {
|
|
|
2166
2243
|
}
|
|
2167
2244
|
this.clearBackDocumentTimer();
|
|
2168
2245
|
this.detectionBoxes = [];
|
|
2169
|
-
this.logger.state('DETECTOR_DETENIDO', { timestamp: Date.now() });
|
|
2170
2246
|
}
|
|
2171
2247
|
startBackDocumentTimer() {
|
|
2172
2248
|
if (!this.enableBackDocumentTimer)
|
|
@@ -2195,6 +2271,15 @@ const JaakStamps = class {
|
|
|
2195
2271
|
return; // Don't toggle if switching camera
|
|
2196
2272
|
this.showCameraSelector = !this.showCameraSelector;
|
|
2197
2273
|
}
|
|
2274
|
+
async updateCameraInfoWithAutofocus() {
|
|
2275
|
+
try {
|
|
2276
|
+
const cameraInfo = await this.cameraService.getCameraInfo();
|
|
2277
|
+
this.cameraInfoWithAutofocus = cameraInfo;
|
|
2278
|
+
}
|
|
2279
|
+
catch (error) {
|
|
2280
|
+
// Keep existing state if update fails
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2198
2283
|
async handleCameraSwitch(cameraId) {
|
|
2199
2284
|
if (this.isSwitchingCamera)
|
|
2200
2285
|
return; // Prevent multiple simultaneous switches
|
|
@@ -2202,10 +2287,6 @@ const JaakStamps = class {
|
|
|
2202
2287
|
// Close the selector immediately when user selects a camera
|
|
2203
2288
|
this.showCameraSelector = false;
|
|
2204
2289
|
this.isSwitchingCamera = true;
|
|
2205
|
-
this.logger.state('INICIANDO_CAMBIO_CAMARA', {
|
|
2206
|
-
from: this.cameraService.getSelectedCameraId(),
|
|
2207
|
-
to: cameraId
|
|
2208
|
-
});
|
|
2209
2290
|
// Stop current video stream
|
|
2210
2291
|
if (this.videoStream) {
|
|
2211
2292
|
this.videoStream.getTracks().forEach(track => track.stop());
|
|
@@ -2216,21 +2297,17 @@ const JaakStamps = class {
|
|
|
2216
2297
|
const newStream = await this.cameraService.setupCamera();
|
|
2217
2298
|
// Update video element
|
|
2218
2299
|
await this.initializeVideoStream(newStream);
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
isRearCamera: this.cameraService.isRearCamera(newStream)
|
|
2222
|
-
});
|
|
2300
|
+
// Update camera info with autofocus data
|
|
2301
|
+
await this.updateCameraInfoWithAutofocus();
|
|
2223
2302
|
}
|
|
2224
2303
|
catch (error) {
|
|
2225
|
-
this.logger.error('Error al cambiar cámara:', error);
|
|
2226
2304
|
// Try to restore previous camera if switch failed
|
|
2227
2305
|
try {
|
|
2228
2306
|
const fallbackStream = await this.cameraService.setupCamera();
|
|
2229
2307
|
await this.initializeVideoStream(fallbackStream);
|
|
2230
2308
|
}
|
|
2231
2309
|
catch (fallbackError) {
|
|
2232
|
-
this.
|
|
2233
|
-
this.updateStatus('Error al cambiar cámara', 'No se pudo completar el cambio de dispositivo', 'error');
|
|
2310
|
+
this.updateStatus('Error al cambiar cámara', 'No se pudo cambiar el dispositivo', 'error');
|
|
2234
2311
|
}
|
|
2235
2312
|
}
|
|
2236
2313
|
finally {
|
|
@@ -2271,7 +2348,7 @@ const JaakStamps = class {
|
|
|
2271
2348
|
this.stateManager.updateCaptureState({ isVideoActive: false, isLoading: false });
|
|
2272
2349
|
}
|
|
2273
2350
|
this.isMaskReady = false;
|
|
2274
|
-
this.updateStatus('
|
|
2351
|
+
this.updateStatus('Captura finalizada', '', 'ready');
|
|
2275
2352
|
this.detectionBoxes = [];
|
|
2276
2353
|
this.cleanup();
|
|
2277
2354
|
}
|
|
@@ -2282,7 +2359,6 @@ const JaakStamps = class {
|
|
|
2282
2359
|
this.performanceUpdateInterval = window.setInterval(() => {
|
|
2283
2360
|
this.updatePerformanceMetrics();
|
|
2284
2361
|
}, 500);
|
|
2285
|
-
this.logger.debug('Monitor de performance inicializado');
|
|
2286
2362
|
}
|
|
2287
2363
|
updatePerformanceMetrics() {
|
|
2288
2364
|
const currentTime = performance.now();
|
|
@@ -2325,6 +2401,66 @@ const JaakStamps = class {
|
|
|
2325
2401
|
if (detectionsFound > 0) {
|
|
2326
2402
|
this.performanceMetrics.successfulDetections++;
|
|
2327
2403
|
}
|
|
2404
|
+
// Update performance history for adaptive frame skipping
|
|
2405
|
+
this.performanceHistory.push(processingTime);
|
|
2406
|
+
if (this.performanceHistory.length > this.PERFORMANCE_HISTORY_SIZE) {
|
|
2407
|
+
this.performanceHistory.shift();
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
// Adaptive frame skipping based on performance
|
|
2411
|
+
getAdaptiveFrameSkip() {
|
|
2412
|
+
// Base conditions for high frame skip
|
|
2413
|
+
if (this.consecutiveFailures > 20)
|
|
2414
|
+
return this.MAX_FRAME_SKIP;
|
|
2415
|
+
if (this.performanceMetrics.inferenceTime > 200)
|
|
2416
|
+
return 6;
|
|
2417
|
+
if (this.performanceMetrics.memoryUsage > 200)
|
|
2418
|
+
return 5;
|
|
2419
|
+
// Calculate average processing time
|
|
2420
|
+
if (this.performanceHistory.length > 0) {
|
|
2421
|
+
const avgProcessingTime = this.performanceHistory.reduce((a, b) => a + b, 0) / this.performanceHistory.length;
|
|
2422
|
+
if (avgProcessingTime > 100)
|
|
2423
|
+
return 4;
|
|
2424
|
+
if (avgProcessingTime > 80)
|
|
2425
|
+
return 3;
|
|
2426
|
+
if (avgProcessingTime > 60)
|
|
2427
|
+
return this.BASE_FRAME_SKIP + 1;
|
|
2428
|
+
}
|
|
2429
|
+
// Low memory devices get higher frame skip
|
|
2430
|
+
if (this.performanceMetrics.memoryUsage > 150)
|
|
2431
|
+
return 4;
|
|
2432
|
+
// Performance is good, use base frame skip
|
|
2433
|
+
return this.BASE_FRAME_SKIP;
|
|
2434
|
+
}
|
|
2435
|
+
// Canvas pool management for screenshots
|
|
2436
|
+
getPooledCanvas(width, height) {
|
|
2437
|
+
// Try to find a canvas with matching dimensions
|
|
2438
|
+
const matchingIndex = this.canvasPool.findIndex(canvas => canvas.width === width && canvas.height === height);
|
|
2439
|
+
if (matchingIndex !== -1) {
|
|
2440
|
+
return this.canvasPool.splice(matchingIndex, 1)[0];
|
|
2441
|
+
}
|
|
2442
|
+
// If no matching canvas, try to reuse any canvas and resize
|
|
2443
|
+
if (this.canvasPool.length > 0) {
|
|
2444
|
+
const canvas = this.canvasPool.pop();
|
|
2445
|
+
canvas.width = width;
|
|
2446
|
+
canvas.height = height;
|
|
2447
|
+
return canvas;
|
|
2448
|
+
}
|
|
2449
|
+
// Create new canvas if pool is empty
|
|
2450
|
+
const canvas = document.createElement('canvas');
|
|
2451
|
+
canvas.width = width;
|
|
2452
|
+
canvas.height = height;
|
|
2453
|
+
return canvas;
|
|
2454
|
+
}
|
|
2455
|
+
returnCanvasToPool(canvas) {
|
|
2456
|
+
// Only return to pool if not at max capacity
|
|
2457
|
+
if (this.canvasPool.length < this.MAX_CANVAS_POOL_SIZE) {
|
|
2458
|
+
// Clear the canvas before returning to pool
|
|
2459
|
+
const ctx = canvas.getContext('2d');
|
|
2460
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
2461
|
+
this.canvasPool.push(canvas);
|
|
2462
|
+
}
|
|
2463
|
+
// If pool is full, let the canvas be garbage collected
|
|
2328
2464
|
}
|
|
2329
2465
|
};
|
|
2330
2466
|
JaakStamps.style = myComponentCss;
|