@smileid/web-components 10.0.3 → 10.0.5
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/dist/esm/{DocumentCaptureScreens-CkWKSrqy.js → DocumentCaptureScreens-BIJUlWLB.js} +98 -56
- package/dist/esm/DocumentCaptureScreens-BIJUlWLB.js.map +1 -0
- package/dist/esm/{EndUserConsent-CMHp-34-.js → EndUserConsent-D4fd1ovG.js} +2 -2
- package/dist/esm/{EndUserConsent-CMHp-34-.js.map → EndUserConsent-D4fd1ovG.js.map} +1 -1
- package/dist/esm/{Navigation-juBE4qOw.js → Navigation-CTjK6tLU.js} +6 -6
- package/dist/esm/{Navigation-juBE4qOw.js.map → Navigation-CTjK6tLU.js.map} +1 -1
- package/dist/esm/SelfieCaptureScreens-CnMaKUmP.js +11361 -0
- package/dist/esm/SelfieCaptureScreens-CnMaKUmP.js.map +1 -0
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/main.js +4 -4
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/{package-CmYr0HUS.js → package-PZvRbm5J.js} +2 -2
- package/dist/esm/{package-CmYr0HUS.js.map → package-PZvRbm5J.js.map} +1 -1
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +12 -6
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/{styles-D2i3GFLK.js → styles-BOEZtbuc.js} +19 -5
- package/dist/esm/{styles-D2i3GFLK.js.map → styles-BOEZtbuc.js.map} +1 -1
- package/dist/smart-camera-web.js +278 -290
- package/dist/smart-camera-web.js.map +1 -1
- package/lib/components/document/src/DocumentCaptureScreens.js +1 -1
- package/lib/components/document/src/document-capture/DocumentCapture.js +2 -1
- package/lib/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +54 -8
- package/lib/components/document/src/document-capture-review/DocumentCaptureReview.js +2 -3
- package/lib/components/navigation/src/Navigation.js +5 -5
- package/lib/components/selfie/src/SelfieCaptureScreens.js +116 -118
- package/lib/components/selfie/src/selfie-capture/SelfieCapture.js +54 -10
- package/lib/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +91 -58
- package/lib/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +23 -154
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +13 -0
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +66 -6
- package/lib/components/selfie/src/smartselfie-capture/components/CaptureControls.tsx +2 -0
- package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +165 -21
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +82 -23
- package/lib/components/selfie/src/smartselfie-capture/utils/alertMessages.ts +2 -1
- package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +18 -1
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +7 -1
- package/lib/styles/src/styles.js +18 -4
- package/package.json +3 -1
- package/dist/esm/DocumentCaptureScreens-CkWKSrqy.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-BF1keQ0h.js +0 -7619
- package/dist/esm/SelfieCaptureScreens-BF1keQ0h.js.map +0 -1
|
@@ -1,12 +1,54 @@
|
|
|
1
|
-
import { useRef, useState } from 'preact/hooks';
|
|
1
|
+
import { useRef, useState, useEffect } from 'preact/hooks';
|
|
2
2
|
|
|
3
|
-
export const useCamera = () => {
|
|
3
|
+
export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
4
4
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
5
5
|
const streamRef = useRef<MediaStream | null>(null);
|
|
6
|
-
const [facingMode, setFacingMode] = useState
|
|
6
|
+
const [facingMode, setFacingMode] = useState(initialFacingMode);
|
|
7
7
|
const [agentSupported, setAgentSupported] = useState(false);
|
|
8
|
+
const onCameraSwitchCallbackRef = useRef<(() => void) | null>(null);
|
|
9
|
+
const isSwitchingCameraRef = useRef(false);
|
|
10
|
+
const timeoutIdsRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
|
8
11
|
|
|
9
|
-
const
|
|
12
|
+
const registerCameraSwitchCallback = (callback: () => void) => {
|
|
13
|
+
onCameraSwitchCallbackRef.current = callback;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const video = videoRef.current;
|
|
18
|
+
if (!video) return undefined;
|
|
19
|
+
|
|
20
|
+
const handleVideoReady = () => {
|
|
21
|
+
if (isSwitchingCameraRef.current && onCameraSwitchCallbackRef.current) {
|
|
22
|
+
const timeoutId = setTimeout(() => {
|
|
23
|
+
onCameraSwitchCallbackRef.current?.();
|
|
24
|
+
isSwitchingCameraRef.current = false;
|
|
25
|
+
timeoutIdsRef.current.delete(timeoutId);
|
|
26
|
+
}, 100);
|
|
27
|
+
timeoutIdsRef.current.add(timeoutId);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
video.addEventListener('loadedmetadata', handleVideoReady);
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
video.removeEventListener('loadedmetadata', handleVideoReady);
|
|
35
|
+
timeoutIdsRef.current.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
36
|
+
timeoutIdsRef.current.clear();
|
|
37
|
+
};
|
|
38
|
+
}, [videoRef.current?.src]);
|
|
39
|
+
|
|
40
|
+
useEffect(
|
|
41
|
+
() => () => {
|
|
42
|
+
timeoutIdsRef.current.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
43
|
+
timeoutIdsRef.current.clear();
|
|
44
|
+
},
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const startCamera = async (
|
|
49
|
+
targetFacingMode?: CameraFacingMode,
|
|
50
|
+
callback?: (cameraName?: string) => void,
|
|
51
|
+
) => {
|
|
10
52
|
try {
|
|
11
53
|
if (streamRef.current) {
|
|
12
54
|
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
@@ -18,10 +60,24 @@ export const useCamera = () => {
|
|
|
18
60
|
}
|
|
19
61
|
|
|
20
62
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
21
|
-
video: { facingMode },
|
|
63
|
+
video: { facingMode: targetFacingMode || facingMode },
|
|
22
64
|
});
|
|
23
65
|
streamRef.current = stream;
|
|
24
66
|
|
|
67
|
+
const track = stream.getVideoTracks()[0];
|
|
68
|
+
const settings = track.getSettings();
|
|
69
|
+
const actualFacingMode = settings.facingMode as
|
|
70
|
+
| CameraFacingMode
|
|
71
|
+
| undefined;
|
|
72
|
+
|
|
73
|
+
const requestedFacingMode = targetFacingMode || facingMode;
|
|
74
|
+
|
|
75
|
+
if (actualFacingMode && actualFacingMode !== requestedFacingMode) {
|
|
76
|
+
setFacingMode(actualFacingMode);
|
|
77
|
+
} else if (actualFacingMode && actualFacingMode !== facingMode) {
|
|
78
|
+
setFacingMode(actualFacingMode);
|
|
79
|
+
}
|
|
80
|
+
|
|
25
81
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
26
82
|
const videoDevice = devices.find(
|
|
27
83
|
(device) =>
|
|
@@ -29,16 +85,12 @@ export const useCamera = () => {
|
|
|
29
85
|
stream.getVideoTracks()[0].getSettings().deviceId === device.deviceId,
|
|
30
86
|
);
|
|
31
87
|
|
|
32
|
-
|
|
33
|
-
smartCameraWeb?.dispatchEvent(
|
|
34
|
-
new CustomEvent('metadata.camera-name', {
|
|
35
|
-
detail: { cameraName: videoDevice?.label },
|
|
36
|
-
}),
|
|
37
|
-
);
|
|
88
|
+
callback?.(videoDevice?.label);
|
|
38
89
|
|
|
39
90
|
if (videoRef.current) {
|
|
40
91
|
videoRef.current.srcObject = stream;
|
|
41
92
|
await videoRef.current.play();
|
|
93
|
+
// Video ready callback will be handled by useEffect
|
|
42
94
|
}
|
|
43
95
|
} catch (error) {
|
|
44
96
|
console.error('Failed to start camera:', error);
|
|
@@ -47,23 +99,114 @@ export const useCamera = () => {
|
|
|
47
99
|
|
|
48
100
|
const switchCamera = async () => {
|
|
49
101
|
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
|
|
50
|
-
|
|
102
|
+
isSwitchingCameraRef.current = true;
|
|
51
103
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
104
|
+
const previousFacingMode = facingMode;
|
|
105
|
+
try {
|
|
106
|
+
setFacingMode(newFacingMode);
|
|
107
|
+
if (streamRef.current) {
|
|
108
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
109
|
+
streamRef.current = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await startCamera(newFacingMode);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
setFacingMode(previousFacingMode);
|
|
115
|
+
isSwitchingCameraRef.current = false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await startCamera(previousFacingMode);
|
|
119
|
+
} catch (restoreError) {
|
|
120
|
+
console.error('Failed to restore previous camera:', restoreError);
|
|
121
|
+
}
|
|
55
122
|
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const detectBrowserEngine = () => {
|
|
126
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
127
|
+
|
|
128
|
+
const isGecko =
|
|
129
|
+
userAgent.includes('firefox') ||
|
|
130
|
+
(userAgent.includes('gecko') &&
|
|
131
|
+
!userAgent.includes('chrome') &&
|
|
132
|
+
!userAgent.includes('edge'));
|
|
56
133
|
|
|
57
|
-
|
|
134
|
+
const hasFirefoxFeatures =
|
|
135
|
+
'mozInnerScreenX' in window ||
|
|
136
|
+
'mozInputSource' in window ||
|
|
137
|
+
'mozPaintCount' in window ||
|
|
138
|
+
typeof (window as any).InstallTrigger !== 'undefined';
|
|
139
|
+
|
|
140
|
+
const supportsMozCSS =
|
|
141
|
+
CSS.supports &&
|
|
142
|
+
(CSS.supports('-moz-appearance', 'none') ||
|
|
143
|
+
CSS.supports('-moz-user-select', 'none'));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
isGecko: isGecko || hasFirefoxFeatures || supportsMozCSS,
|
|
147
|
+
isChromium:
|
|
148
|
+
userAgent.includes('chrome') ||
|
|
149
|
+
userAgent.includes('chromium') ||
|
|
150
|
+
userAgent.includes('edge'),
|
|
151
|
+
isWebKit: userAgent.includes('webkit') && !userAgent.includes('chrome'),
|
|
152
|
+
};
|
|
58
153
|
};
|
|
59
154
|
|
|
60
155
|
const checkAgentSupport = async () => {
|
|
61
156
|
try {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
157
|
+
const isMobile =
|
|
158
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
159
|
+
navigator.userAgent,
|
|
160
|
+
) ||
|
|
161
|
+
(navigator.maxTouchPoints && navigator.maxTouchPoints > 1);
|
|
162
|
+
|
|
163
|
+
// mobile devices generally support both cameras
|
|
164
|
+
// also, ios crashes if we try to check for cameras
|
|
165
|
+
if (isMobile) {
|
|
166
|
+
setAgentSupported(true);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { isGecko } = detectBrowserEngine();
|
|
171
|
+
|
|
172
|
+
let userCameraId: string | null = null;
|
|
173
|
+
let environmentCameraId: string | null = null;
|
|
174
|
+
|
|
175
|
+
// test if we can get a user-facing camera
|
|
176
|
+
try {
|
|
177
|
+
const userStream = await navigator.mediaDevices.getUserMedia({
|
|
178
|
+
video: { facingMode: 'user' },
|
|
179
|
+
});
|
|
180
|
+
userCameraId =
|
|
181
|
+
userStream.getVideoTracks()[0].getSettings().deviceId ?? null;
|
|
182
|
+
userStream.getTracks().forEach((track) => track.stop());
|
|
183
|
+
} catch (error) {
|
|
184
|
+
// no user-facing camera available
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// test if we can get an environment-facing camera
|
|
188
|
+
try {
|
|
189
|
+
const envStream = await navigator.mediaDevices.getUserMedia({
|
|
190
|
+
video: { facingMode: 'environment' },
|
|
191
|
+
});
|
|
192
|
+
environmentCameraId =
|
|
193
|
+
envStream.getVideoTracks()[0].getSettings().deviceId ?? null;
|
|
194
|
+
envStream.getTracks().forEach((track) => track.stop());
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// no environment-facing camera available
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hasBothCameras =
|
|
200
|
+
userCameraId &&
|
|
201
|
+
environmentCameraId &&
|
|
202
|
+
userCameraId !== environmentCameraId;
|
|
203
|
+
|
|
204
|
+
if (!hasBothCameras) {
|
|
205
|
+
setAgentSupported(false);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setAgentSupported(!isGecko);
|
|
67
210
|
} catch (error) {
|
|
68
211
|
setAgentSupported(false);
|
|
69
212
|
}
|
|
@@ -90,5 +233,6 @@ export const useCamera = () => {
|
|
|
90
233
|
switchCamera,
|
|
91
234
|
checkAgentSupport,
|
|
92
235
|
stopCamera,
|
|
236
|
+
registerCameraSwitchCallback,
|
|
93
237
|
};
|
|
94
238
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef } from 'preact/hooks';
|
|
2
2
|
import { useSignal, useComputed } from '@preact/signals';
|
|
3
3
|
import { FaceLandmarker } from '@mediapipe/tasks-vision';
|
|
4
|
+
import { throttle } from 'lodash';
|
|
4
5
|
import {
|
|
5
6
|
calculateFaceSize,
|
|
6
7
|
isFaceInBounds,
|
|
@@ -29,6 +30,7 @@ interface UseFaceCaptureProps {
|
|
|
29
30
|
minFaceSize: number;
|
|
30
31
|
maxFaceSize: number;
|
|
31
32
|
smileCooldown: number;
|
|
33
|
+
getFacingMode: () => CameraFacingMode;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export const useFaceCapture = ({
|
|
@@ -41,6 +43,7 @@ export const useFaceCapture = ({
|
|
|
41
43
|
minFaceSize,
|
|
42
44
|
maxFaceSize,
|
|
43
45
|
smileCooldown,
|
|
46
|
+
getFacingMode,
|
|
44
47
|
}: UseFaceCaptureProps) => {
|
|
45
48
|
const faceLandmarkerRef = useRef<FaceLandmarker | null>(null);
|
|
46
49
|
const animationFrameRef = useRef<number | null>(null);
|
|
@@ -58,6 +61,7 @@ export const useFaceCapture = ({
|
|
|
58
61
|
const currentMouthOpen = useSignal(0);
|
|
59
62
|
const lastSmileTime = useSignal(0);
|
|
60
63
|
const alertTitle = useSignal('');
|
|
64
|
+
const isInitializing = useSignal(true);
|
|
61
65
|
|
|
62
66
|
const isCapturing = useSignal(false);
|
|
63
67
|
const isPaused = useSignal(false);
|
|
@@ -81,19 +85,36 @@ export const useFaceCapture = ({
|
|
|
81
85
|
!multipleFaces.value,
|
|
82
86
|
);
|
|
83
87
|
|
|
88
|
+
const updateAlertImmediate = (messageKey: MessageKey | null) => {
|
|
89
|
+
if (messageKey && MESSAGES[messageKey]) {
|
|
90
|
+
alertTitle.value = MESSAGES[messageKey];
|
|
91
|
+
} else {
|
|
92
|
+
alertTitle.value = '';
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const updateAlert = useRef(
|
|
97
|
+
throttle((messageKey: MessageKey | null) => {
|
|
98
|
+
updateAlertImmediate(messageKey);
|
|
99
|
+
}, 300),
|
|
100
|
+
).current;
|
|
101
|
+
|
|
84
102
|
const initializeFaceLandmarker = async () => {
|
|
85
103
|
try {
|
|
104
|
+
const isAlreadyLoaded =
|
|
105
|
+
window.__smileIdentityMediapipe?.loaded &&
|
|
106
|
+
window.__smileIdentityMediapipe?.instance;
|
|
107
|
+
|
|
108
|
+
if (!isAlreadyLoaded) {
|
|
109
|
+
isInitializing.value = true;
|
|
110
|
+
updateAlertImmediate('initializing');
|
|
111
|
+
}
|
|
112
|
+
|
|
86
113
|
faceLandmarkerRef.current = await getMediapipeInstance();
|
|
114
|
+
isInitializing.value = false;
|
|
87
115
|
} catch (error) {
|
|
88
116
|
console.error('Failed to initialize MediaPipe:', error);
|
|
89
|
-
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const updateAlert = (messageKey: MessageKey | null) => {
|
|
93
|
-
if (messageKey && MESSAGES[messageKey]) {
|
|
94
|
-
alertTitle.value = MESSAGES[messageKey];
|
|
95
|
-
} else {
|
|
96
|
-
alertTitle.value = '';
|
|
117
|
+
isInitializing.value = false;
|
|
97
118
|
}
|
|
98
119
|
};
|
|
99
120
|
|
|
@@ -110,7 +131,6 @@ export const useFaceCapture = ({
|
|
|
110
131
|
if (container) {
|
|
111
132
|
canvasRef.current.style.left = '50%';
|
|
112
133
|
canvasRef.current.style.top = '50%';
|
|
113
|
-
canvasRef.current.style.transform = 'translate(-50%, -50%) scaleX(-1)';
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
};
|
|
@@ -119,9 +139,7 @@ export const useFaceCapture = ({
|
|
|
119
139
|
const isInNeutralZone = capturesTaken.value < neutralZone.value;
|
|
120
140
|
const isInSmileZone = capturesTaken.value >= smileCheckpoint.value;
|
|
121
141
|
|
|
122
|
-
if (isInNeutralZone
|
|
123
|
-
updateAlert('neutral-expression');
|
|
124
|
-
} else if (isInNeutralZone) {
|
|
142
|
+
if (isInNeutralZone) {
|
|
125
143
|
alertTitle.value = 'Capturing...';
|
|
126
144
|
} else if (isInSmileZone) {
|
|
127
145
|
const timeSinceSmile = Date.now() - lastSmileTime.value;
|
|
@@ -143,7 +161,9 @@ export const useFaceCapture = ({
|
|
|
143
161
|
};
|
|
144
162
|
|
|
145
163
|
const updateAlerts = () => {
|
|
146
|
-
if (
|
|
164
|
+
if (isInitializing.value) {
|
|
165
|
+
updateAlertImmediate('initializing');
|
|
166
|
+
} else if (multipleFaces.value) {
|
|
147
167
|
updateAlert('multiple-faces');
|
|
148
168
|
} else if (!faceDetected.value) {
|
|
149
169
|
updateAlert('no-face');
|
|
@@ -173,7 +193,21 @@ export const useFaceCapture = ({
|
|
|
173
193
|
return;
|
|
174
194
|
}
|
|
175
195
|
|
|
196
|
+
// ensure video has valid dimensions before processing
|
|
197
|
+
if (
|
|
198
|
+
videoRef.current.videoWidth <= 0 ||
|
|
199
|
+
videoRef.current.videoHeight <= 0 ||
|
|
200
|
+
videoRef.current.readyState < 2
|
|
201
|
+
) {
|
|
202
|
+
animationFrameRef.current = requestAnimationFrame(detectFace);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
176
206
|
try {
|
|
207
|
+
if (isInitializing.value) {
|
|
208
|
+
isInitializing.value = false;
|
|
209
|
+
}
|
|
210
|
+
|
|
177
211
|
const croppedCanvas = createCroppedVideoFrame(videoRef.current);
|
|
178
212
|
const detectionSource = croppedCanvas || videoRef.current;
|
|
179
213
|
|
|
@@ -359,6 +393,7 @@ export const useFaceCapture = ({
|
|
|
359
393
|
images: [...livenessImages, referenceImage],
|
|
360
394
|
referenceImage: referencePhoto.value,
|
|
361
395
|
previewImage: referencePhoto.value,
|
|
396
|
+
facingMode: getFacingMode(),
|
|
362
397
|
meta: { libraryVersion: COMPONENTS_VERSION },
|
|
363
398
|
};
|
|
364
399
|
|
|
@@ -368,11 +403,6 @@ export const useFaceCapture = ({
|
|
|
368
403
|
}),
|
|
369
404
|
);
|
|
370
405
|
|
|
371
|
-
const smartCameraWeb = document.querySelector('smart-camera-web');
|
|
372
|
-
smartCameraWeb?.dispatchEvent(
|
|
373
|
-
new CustomEvent('metadata.selfie-capture-end'),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
406
|
hasFinishedCapture.value = true;
|
|
377
407
|
}
|
|
378
408
|
};
|
|
@@ -424,13 +454,8 @@ export const useFaceCapture = ({
|
|
|
424
454
|
return;
|
|
425
455
|
}
|
|
426
456
|
|
|
427
|
-
const isInNeutralZone = capturesTaken.value < neutralZone.value;
|
|
428
457
|
const isInSmileZone = capturesTaken.value >= smileCheckpoint.value;
|
|
429
458
|
|
|
430
|
-
if (isInNeutralZone && currentSmileScore.value >= smileThreshold) {
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
459
|
if (isInSmileZone) {
|
|
435
460
|
const timeSinceSmile = Date.now() - lastSmileTime.value;
|
|
436
461
|
if (timeSinceSmile > smileCooldown) {
|
|
@@ -474,6 +499,21 @@ export const useFaceCapture = ({
|
|
|
474
499
|
capturesTaken.value = 0;
|
|
475
500
|
countdown.value = totalCaptures.value;
|
|
476
501
|
|
|
502
|
+
const smartCameraWeb = document.querySelector('smart-camera-web');
|
|
503
|
+
smartCameraWeb?.dispatchEvent(
|
|
504
|
+
new CustomEvent('metadata.selfie-capture-start'),
|
|
505
|
+
);
|
|
506
|
+
smartCameraWeb?.dispatchEvent(
|
|
507
|
+
new CustomEvent('metadata.selfie-origin', {
|
|
508
|
+
detail: {
|
|
509
|
+
imageOrigin: {
|
|
510
|
+
environment: 'back_camera',
|
|
511
|
+
user: 'front_camera',
|
|
512
|
+
}[getFacingMode()],
|
|
513
|
+
},
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
|
|
477
517
|
startCaptureInterval();
|
|
478
518
|
};
|
|
479
519
|
|
|
@@ -515,6 +555,23 @@ export const useFaceCapture = ({
|
|
|
515
555
|
clearInterval(captureTimerRef.current);
|
|
516
556
|
}
|
|
517
557
|
stopDetectionLoop();
|
|
558
|
+
updateAlert.cancel();
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const resetFaceDetectionState = () => {
|
|
562
|
+
faceDetected.value = false;
|
|
563
|
+
faceInBounds.value = false;
|
|
564
|
+
faceProximity.value = 'good';
|
|
565
|
+
multipleFaces.value = false;
|
|
566
|
+
faceLandmarks.value = [];
|
|
567
|
+
currentSmileScore.value = 0;
|
|
568
|
+
currentFaceSize.value = 0;
|
|
569
|
+
currentMouthOpen.value = 0;
|
|
570
|
+
lastSmileTime.value = 0;
|
|
571
|
+
|
|
572
|
+
if (canvasRef.current) {
|
|
573
|
+
clearCanvas(canvasRef.current);
|
|
574
|
+
}
|
|
518
575
|
};
|
|
519
576
|
|
|
520
577
|
return {
|
|
@@ -529,6 +586,7 @@ export const useFaceCapture = ({
|
|
|
529
586
|
currentMouthOpen,
|
|
530
587
|
lastSmileTime,
|
|
531
588
|
alertTitle,
|
|
589
|
+
isInitializing,
|
|
532
590
|
isReadyToCapture,
|
|
533
591
|
|
|
534
592
|
isCapturing,
|
|
@@ -554,5 +612,6 @@ export const useFaceCapture = ({
|
|
|
554
612
|
handleCancel,
|
|
555
613
|
handleClose,
|
|
556
614
|
cleanup,
|
|
615
|
+
resetFaceDetectionState,
|
|
557
616
|
};
|
|
558
617
|
};
|
|
@@ -6,7 +6,8 @@ export const MESSAGES = {
|
|
|
6
6
|
'too-far': 'Move closer',
|
|
7
7
|
'neutral-expression': 'Neutral expression',
|
|
8
8
|
'smile-required': 'Smile!',
|
|
9
|
-
'open-mouth-smile': '
|
|
9
|
+
'open-mouth-smile': 'Wider smile - teeth visible',
|
|
10
|
+
initializing: 'Initializing...',
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export type MessageKey = keyof typeof MESSAGES;
|
|
@@ -10,6 +10,23 @@ declare global {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const hasFP16Support = () => {
|
|
14
|
+
const canvas = document.createElement('canvas');
|
|
15
|
+
const gl =
|
|
16
|
+
canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
17
|
+
if (!gl) return false;
|
|
18
|
+
|
|
19
|
+
const hasHalfFloatExt = (gl as any).getExtension('OES_texture_half_float');
|
|
20
|
+
const hasHalfFloatLinear = (gl as any).getExtension(
|
|
21
|
+
'OES_texture_half_float_linear',
|
|
22
|
+
);
|
|
23
|
+
const hasColorBufferHalfFloat = (gl as any).getExtension(
|
|
24
|
+
'EXT_color_buffer_half_float',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return !!(hasHalfFloatExt && hasColorBufferHalfFloat && hasHalfFloatLinear);
|
|
28
|
+
};
|
|
29
|
+
|
|
13
30
|
export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
14
31
|
if (!window.__smileIdentityMediapipe) {
|
|
15
32
|
window.__smileIdentityMediapipe = {
|
|
@@ -38,7 +55,7 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
|
38
55
|
const faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
|
|
39
56
|
baseOptions: {
|
|
40
57
|
modelAssetPath: `https://web-models.smileidentity.com/face_landmarker/face_landmarker.task`,
|
|
41
|
-
delegate: 'GPU',
|
|
58
|
+
delegate: hasFP16Support() ? 'GPU' : 'CPU',
|
|
42
59
|
},
|
|
43
60
|
outputFaceBlendshapes: true,
|
|
44
61
|
runningMode: 'VIDEO',
|
|
@@ -10,8 +10,14 @@ const COMPONENTS_VERSION = packageJson.version;
|
|
|
10
10
|
|
|
11
11
|
function scwTemplateString() {
|
|
12
12
|
return `
|
|
13
|
+
<style>
|
|
14
|
+
:host {
|
|
15
|
+
display: block;
|
|
16
|
+
height: 100%;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
13
19
|
${styles(this.themeColor)}
|
|
14
|
-
<div>
|
|
20
|
+
<div style="height: 100%;">
|
|
15
21
|
<camera-permission ${this.applyComponentThemeColor} ${this.title} ${this.showNavigation} ${this.hideInstructions ? '' : 'hidden'} ${this.hideAttribution}></camera-permission>
|
|
16
22
|
<selfie-capture-screens ${this.applyComponentThemeColor} ${this.title} ${this.showNavigation} ${this.disableImageTests} ${this.hideAttribution} ${this.hideInstructions} hidden
|
|
17
23
|
${this.hideBackToHost} ${this.allowAgentMode} ${this.allowAgentModeTests}
|
package/lib/styles/src/styles.js
CHANGED
|
@@ -260,6 +260,9 @@ ${typography}
|
|
|
260
260
|
|
|
261
261
|
#document-capture-instructions-screen,
|
|
262
262
|
#back-of-document-capture-instructions-screen {
|
|
263
|
+
box-sizing: border-box;
|
|
264
|
+
height: 100%;
|
|
265
|
+
padding: 1rem;
|
|
263
266
|
display: flex;
|
|
264
267
|
flex-direction: column;
|
|
265
268
|
max-block-size: 100%;
|
|
@@ -287,22 +290,33 @@ ${typography}
|
|
|
287
290
|
padding-bottom: 2rem;
|
|
288
291
|
}
|
|
289
292
|
|
|
293
|
+
smart-camera-web, selfie-capture-screens, selfie-capture-instructions, document-capture-screens, document-capture-instructions {
|
|
294
|
+
height: 100%;
|
|
295
|
+
display: block;
|
|
296
|
+
}
|
|
297
|
+
|
|
290
298
|
.instructions-wrapper {
|
|
291
299
|
display: inline-flex;
|
|
292
300
|
flex-direction: column;
|
|
293
|
-
gap:
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
gap: 1rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@media (min-width: 40rem) {
|
|
305
|
+
.instructions-wrapper {
|
|
306
|
+
gap: 1.75rem;
|
|
307
|
+
}
|
|
296
308
|
}
|
|
309
|
+
|
|
297
310
|
.instructions {
|
|
298
311
|
display: flex;
|
|
299
312
|
align-items: center;
|
|
300
313
|
text-align: initial;
|
|
314
|
+
gap: 0.5rem;
|
|
301
315
|
}
|
|
302
316
|
|
|
303
317
|
.instructions svg {
|
|
304
318
|
flex-shrink: 0;
|
|
305
|
-
margin-inline-end:
|
|
319
|
+
margin-inline-end: 0.25rem;
|
|
306
320
|
}
|
|
307
321
|
|
|
308
322
|
.instructions p {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/web-components",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "dist/esm/main.js",
|
|
6
6
|
"module": "dist/esm/main.js",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
|
|
77
77
|
"@preact/signals": "^2.1.1",
|
|
78
78
|
"@tabler/icons-preact": "^3.34.0",
|
|
79
|
+
"lodash": "^4.17.21",
|
|
79
80
|
"preact": "^10.26.9",
|
|
80
81
|
"preact-custom-element": "^4.3.0",
|
|
81
82
|
"preact-router": "^4.1.2",
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
},
|
|
85
86
|
"devDependencies": {
|
|
86
87
|
"@preact/preset-vite": "^2.10.2",
|
|
88
|
+
"@types/lodash": "^4.17.20",
|
|
87
89
|
"@types/node": "^20.11.24",
|
|
88
90
|
"@types/preact-custom-element": "^4.0.4",
|
|
89
91
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|