@smileid/web-components 11.4.1 → 11.4.3
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-wvJcTVkA.js → DocumentCaptureScreens-D1oMAv4n.js} +4 -4
- package/dist/esm/{DocumentCaptureScreens-wvJcTVkA.js.map → DocumentCaptureScreens-D1oMAv4n.js.map} +1 -1
- package/dist/esm/{EndUserConsent-BXvS7t8z.js → EndUserConsent-D26UoVk5.js} +3 -3
- package/dist/esm/{EndUserConsent-BXvS7t8z.js.map → EndUserConsent-D26UoVk5.js.map} +1 -1
- package/dist/esm/{Navigation-BRFmg7s1.js → Navigation-nvehze1F.js} +2 -2
- package/dist/esm/{Navigation-BRFmg7s1.js.map → Navigation-nvehze1F.js.map} +1 -1
- package/dist/esm/{SelfieCaptureScreens-BkJBfzHv.js → SelfieCaptureScreens-CC-y0CpT.js} +1900 -1794
- package/dist/esm/SelfieCaptureScreens-CC-y0CpT.js.map +1 -0
- package/dist/esm/{TotpConsent-Cn2DkVza.js → TotpConsent-owUOdKzP.js} +2 -2
- package/dist/esm/{TotpConsent-Cn2DkVza.js.map → TotpConsent-owUOdKzP.js.map} +1 -1
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/{index-DBUdxnp9.js → index-5Nn2kzHI.js} +4 -4
- package/dist/esm/{index-DBUdxnp9.js.map → index-5Nn2kzHI.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +6 -6
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/{package-Dax8ezDK.js → package-BxstV9r_.js} +3 -3
- package/dist/esm/{package-Dax8ezDK.js.map → package-BxstV9r_.js.map} +1 -1
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +5 -5
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +61 -52
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +4 -1
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +113 -8
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +72 -39
- package/lib/components/selfie/src/smartselfie-capture/hooks/useCamera.ts +12 -0
- package/lib/components/selfie/src/smartselfie-capture/utils/mediapipeManager.ts +65 -0
- package/lib/components/signature-pad/package.json +2 -2
- package/package.json +10 -1
- package/dist/esm/SelfieCaptureScreens-BkJBfzHv.js.map +0 -1
package/dist/types/main.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ declare class ComboboxRoot extends HTMLElement {
|
|
|
38
38
|
declare class ComboboxTrigger extends HTMLElement {
|
|
39
39
|
handleKeyUp(event: any): void;
|
|
40
40
|
handleKeyDown(event: any): void;
|
|
41
|
+
handleInput(event: any): void;
|
|
42
|
+
handleChange(event: any): void;
|
|
41
43
|
handleSelection(event: any): void;
|
|
42
44
|
toggleExpansionState(): void;
|
|
43
45
|
get type(): string;
|
|
@@ -87,13 +89,13 @@ export declare class DocumentCaptureScreens extends HTMLElement {
|
|
|
87
89
|
setUpEventListeners(): void;
|
|
88
90
|
_publishSelectedImages(): void;
|
|
89
91
|
get hideInstructions(): boolean;
|
|
90
|
-
get autoCapture(): boolean;
|
|
91
92
|
get hideBackOfId(): boolean;
|
|
92
93
|
get showNavigation(): "" | "show-navigation";
|
|
93
94
|
get title(): string;
|
|
94
95
|
get documentCaptureModes(): string;
|
|
95
96
|
get documentType(): string;
|
|
96
97
|
get hideAttribution(): "" | "hide-attribution";
|
|
98
|
+
get newInstructions(): boolean;
|
|
97
99
|
get themeColor(): string;
|
|
98
100
|
handleBackEvents(): void;
|
|
99
101
|
handleCloseEvents(): void;
|
|
@@ -329,6 +331,7 @@ export declare class SmartCameraWeb extends HTMLElement {
|
|
|
329
331
|
get isPortraitCaptureView(): boolean;
|
|
330
332
|
get hideInstructions(): "" | "hide-instructions";
|
|
331
333
|
get hideBackOfId(): "" | "hide-back-of-id";
|
|
334
|
+
get newInstructions(): "" | "new-instructions";
|
|
332
335
|
get showNavigation(): "" | "show-navigation";
|
|
333
336
|
get hideBackToHost(): "" | "hide-back";
|
|
334
337
|
get allowAgentMode(): string;
|
|
@@ -9,7 +9,25 @@ import SmartSelfieCapture from '../smartselfie-capture/SmartSelfieCapture';
|
|
|
9
9
|
// Legacy web component fallback (used when Mediapipe isn't available)
|
|
10
10
|
import '../selfie-capture/SelfieCapture';
|
|
11
11
|
// Mediapipe loader/manager used by SmartSelfieCapture
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getMediapipeInstance,
|
|
14
|
+
UnsupportedMediapipeEnvironmentError,
|
|
15
|
+
} from '../smartselfie-capture/utils/mediapipeManager';
|
|
16
|
+
|
|
17
|
+
// Minimal typing for the optional Sentry SDK that host pages may expose on
|
|
18
|
+
// `window`. We only depend on `captureException`, so keep the surface tight.
|
|
19
|
+
// Sentry tag values are expected to be strings, so the type enforces that.
|
|
20
|
+
type SentryTags = Record<string, string>;
|
|
21
|
+
declare global {
|
|
22
|
+
interface Window {
|
|
23
|
+
Sentry?: {
|
|
24
|
+
captureException: (
|
|
25
|
+
error: unknown,
|
|
26
|
+
context?: { tags?: SentryTags },
|
|
27
|
+
) => void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
13
31
|
|
|
14
32
|
interface Props {
|
|
15
33
|
timeout?: number;
|
|
@@ -29,6 +47,12 @@ interface Props {
|
|
|
29
47
|
|
|
30
48
|
const DEFAULT_MEDIAPIPE_WAIT_MS = 90 * 1000; // For when legacy fallback is NOT allowed, we wait the full 90s for mediapipe to load before showing an error.
|
|
31
49
|
const DEFAULT_WAIT_MS = 20 * 1000; // default for when legacy fallback is allowed we wait for 20s
|
|
50
|
+
// Cap retries on transient init failures so we don't spin forever, while still
|
|
51
|
+
// allowing recovery from short-lived issues (e.g. CDN hiccups while the
|
|
52
|
+
// wrapper is preloading in a hidden state). Retries are spaced with
|
|
53
|
+
// exponential backoff (base * 2^(attempt-1)) so we don't hammer the CDN.
|
|
54
|
+
const MAX_MEDIAPIPE_INIT_ATTEMPTS = 3;
|
|
55
|
+
const MEDIAPIPE_RETRY_BASE_DELAY_MS = 500;
|
|
32
56
|
|
|
33
57
|
// Wrapper component that decides whether to use the modern
|
|
34
58
|
// SmartSelfieCapture (Mediapipe-based) or fallback to the legacy `selfie-capture`
|
|
@@ -73,29 +97,110 @@ const SelfieCaptureWrapper: FunctionComponent<Props> = ({
|
|
|
73
97
|
const [loadingProgress, setLoadingProgress] = useState(isCypress ? 100 : 0);
|
|
74
98
|
const [initialSessionCompleted, setInitialSessionCompleted] = useState(false);
|
|
75
99
|
const [mediapipeLoading, setMediapipeLoading] = useState(false);
|
|
100
|
+
// `unsupportedEnvironment` is a permanent, one-shot signal: we know
|
|
101
|
+
// MediaPipe cannot run here, so stop trying.
|
|
102
|
+
const [unsupportedEnvironment, setUnsupportedEnvironment] = useState(false);
|
|
103
|
+
// Bounded retry counter for transient init failures.
|
|
104
|
+
const [mediapipeInitAttempts, setMediapipeInitAttempts] = useState(0);
|
|
105
|
+
// Dedup flag so we only report a given init failure to Sentry once per
|
|
106
|
+
// wrapper instance, even if we end up retrying.
|
|
107
|
+
const [mediapipeInitReported, setMediapipeInitReported] = useState(false);
|
|
76
108
|
const [usingSelfieCapture, setUsingSelfieCapture] = useState(false);
|
|
77
109
|
|
|
78
|
-
// Attempt to load Mediapipe (
|
|
79
|
-
//
|
|
80
|
-
//
|
|
110
|
+
// Attempt to load Mediapipe (with a small bounded retry budget). If
|
|
111
|
+
// Mediapipe is already ready, currently loading, the environment is
|
|
112
|
+
// definitively unsupported, we've exhausted our retry budget, or we're
|
|
113
|
+
// running under Cypress, skip the attempt. On transient failure we wait
|
|
114
|
+
// (exponential backoff) before allowing the effect to re-run.
|
|
81
115
|
useEffect(() => {
|
|
82
|
-
if (
|
|
116
|
+
if (
|
|
117
|
+
mediapipeReady ||
|
|
118
|
+
mediapipeLoading ||
|
|
119
|
+
unsupportedEnvironment ||
|
|
120
|
+
mediapipeInitAttempts >= MAX_MEDIAPIPE_INIT_ATTEMPTS ||
|
|
121
|
+
isCypress
|
|
122
|
+
)
|
|
123
|
+
return undefined;
|
|
124
|
+
|
|
125
|
+
let cancelled = false;
|
|
126
|
+
let retryTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
83
127
|
|
|
84
128
|
const loadMediapipe = async () => {
|
|
85
129
|
setMediapipeLoading(true);
|
|
130
|
+
const attemptNumber = mediapipeInitAttempts + 1;
|
|
131
|
+
setMediapipeInitAttempts(attemptNumber);
|
|
86
132
|
try {
|
|
87
133
|
await getMediapipeInstance();
|
|
134
|
+
if (cancelled) return;
|
|
88
135
|
setMediapipeReady(true);
|
|
136
|
+
setMediapipeLoading(false);
|
|
89
137
|
} catch (error) {
|
|
138
|
+
if (cancelled) return;
|
|
90
139
|
// Loading failed; we'll fall back to the legacy selfie-capture component
|
|
91
|
-
// after the loadingProgress reaches 100
|
|
140
|
+
// after the loadingProgress reaches 100% (or sooner for definitively
|
|
141
|
+
// unsupported environments — see below).
|
|
92
142
|
console.error('Failed to load Mediapipe:', error);
|
|
143
|
+
const isUnsupportedEnvironment =
|
|
144
|
+
error instanceof UnsupportedMediapipeEnvironmentError;
|
|
145
|
+
// Report to Sentry (when the host page has exposed it on window) so we
|
|
146
|
+
// can observe how often users land on the fallback path and which
|
|
147
|
+
// environments are affected. Dedup so retries don't flood Sentry.
|
|
148
|
+
if (!mediapipeInitReported) {
|
|
149
|
+
setMediapipeInitReported(true);
|
|
150
|
+
window.Sentry?.captureException(error, {
|
|
151
|
+
tags: {
|
|
152
|
+
area: 'mediapipe_init',
|
|
153
|
+
mediapipe_unsupported_environment: isUnsupportedEnvironment
|
|
154
|
+
? 'true'
|
|
155
|
+
: 'false',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// When the environment definitively cannot run MediaPipe (e.g. no
|
|
160
|
+
// WebAssembly reftypes support), there is no point retrying or keeping
|
|
161
|
+
// the user staring at the loading spinner for the full countdown —
|
|
162
|
+
// mark as unsupported and short-circuit to the fallback decision
|
|
163
|
+
// immediately.
|
|
164
|
+
if (isUnsupportedEnvironment) {
|
|
165
|
+
setUnsupportedEnvironment(true);
|
|
166
|
+
setLoadingProgress(100);
|
|
167
|
+
setMediapipeLoading(false);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Transient failure: wait with exponential backoff before allowing the
|
|
171
|
+
// effect to re-run by flipping mediapipeLoading back to false. If
|
|
172
|
+
// we've exhausted our retry budget, just release the loading flag so
|
|
173
|
+
// the countdown / fallback UI can proceed.
|
|
174
|
+
const hasRetriesLeft = attemptNumber < MAX_MEDIAPIPE_INIT_ATTEMPTS;
|
|
175
|
+
if (!hasRetriesLeft) {
|
|
176
|
+
setMediapipeLoading(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const backoffMs =
|
|
180
|
+
MEDIAPIPE_RETRY_BASE_DELAY_MS * 2 ** (attemptNumber - 1);
|
|
181
|
+
retryTimeoutId = setTimeout(() => {
|
|
182
|
+
retryTimeoutId = null;
|
|
183
|
+
if (cancelled) return;
|
|
184
|
+
setMediapipeLoading(false);
|
|
185
|
+
}, backoffMs);
|
|
93
186
|
}
|
|
94
|
-
setMediapipeLoading(false);
|
|
95
187
|
};
|
|
96
188
|
|
|
97
189
|
loadMediapipe();
|
|
98
|
-
|
|
190
|
+
|
|
191
|
+
return () => {
|
|
192
|
+
cancelled = true;
|
|
193
|
+
if (retryTimeoutId !== null) {
|
|
194
|
+
clearTimeout(retryTimeoutId);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}, [
|
|
198
|
+
mediapipeReady,
|
|
199
|
+
mediapipeLoading,
|
|
200
|
+
unsupportedEnvironment,
|
|
201
|
+
mediapipeInitAttempts,
|
|
202
|
+
mediapipeInitReported,
|
|
203
|
+
]);
|
|
99
204
|
|
|
100
205
|
// When using the loading countdown (startCountdown), increment the
|
|
101
206
|
// visible loading progress. This is only used while mediapipe hasn't
|
|
@@ -3,6 +3,7 @@ import register from 'preact-custom-element';
|
|
|
3
3
|
import type { FunctionComponent } from 'preact';
|
|
4
4
|
|
|
5
5
|
import { getBoolProp } from '../../../../utils/props';
|
|
6
|
+
import { translate } from '../../../../domain/localisation';
|
|
6
7
|
import { useFaceCapture, useCamera } from './hooks';
|
|
7
8
|
import { CameraPreview } from './components/CameraPreview';
|
|
8
9
|
import { AlertDisplay } from './components/AlertDisplay';
|
|
@@ -61,8 +62,8 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
61
62
|
getFacingMode: () => camera.facingMode,
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const initializeCamera = async () => {
|
|
66
|
+
try {
|
|
66
67
|
await camera.startCamera(initialFacingMode, (cameraName) => {
|
|
67
68
|
const smartCameraWeb = document.querySelector('smart-camera-web');
|
|
68
69
|
smartCameraWeb?.dispatchEvent(
|
|
@@ -71,15 +72,20 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
71
72
|
}),
|
|
72
73
|
);
|
|
73
74
|
});
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
} catch {
|
|
76
|
+
// startCamera already set cameraError; surface UI and skip downstream init
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await camera.checkAgentSupport();
|
|
80
|
+
await faceCapture.initializeFaceLandmarker();
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
faceCapture.setupCanvas();
|
|
84
|
+
faceCapture.startDetectionLoop();
|
|
85
|
+
}, 500);
|
|
86
|
+
};
|
|
82
87
|
|
|
88
|
+
useEffect(() => {
|
|
83
89
|
camera.registerCameraSwitchCallback(() => {
|
|
84
90
|
try {
|
|
85
91
|
faceCapture.resetFaceDetectionState();
|
|
@@ -142,39 +148,57 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
142
148
|
<smileid-navigation ref={navigationRef} theme-color={themeColor} />
|
|
143
149
|
)}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
isCapturing={faceCapture.isCapturing.value}
|
|
164
|
-
hasFinishedCapture={faceCapture.hasFinishedCapture.value}
|
|
165
|
-
isReadyToCapture={faceCapture.isReadyToCapture.value}
|
|
166
|
-
captureButtonFallbackEnabled={
|
|
167
|
-
faceCapture.captureButtonFallbackEnabled.value
|
|
168
|
-
}
|
|
169
|
-
allowAgentMode={allowAgentMode}
|
|
170
|
-
agentSupported={camera.agentSupported}
|
|
171
|
-
showAgentModeForTests={showAgentModeForTests}
|
|
151
|
+
{camera.cameraError ? (
|
|
152
|
+
<div className="camera-error" role="alert">
|
|
153
|
+
<p>{camera.cameraError}</p>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="btn-primary"
|
|
157
|
+
onClick={() => {
|
|
158
|
+
initializeCamera();
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{translate('camera.error.retry')}
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<>
|
|
166
|
+
<CameraPreview
|
|
167
|
+
videoRef={camera.videoRef}
|
|
168
|
+
canvasRef={canvasRef}
|
|
172
169
|
facingMode={camera.facingMode}
|
|
170
|
+
progress={
|
|
171
|
+
faceCapture.capturesTaken.value > 0
|
|
172
|
+
? faceCapture.capturesTaken.value /
|
|
173
|
+
faceCapture.totalCaptures.value
|
|
174
|
+
: 0
|
|
175
|
+
}
|
|
176
|
+
interval={interval}
|
|
173
177
|
themeColor={themeColor}
|
|
174
|
-
onStartCapture={faceCapture.startCapture}
|
|
175
|
-
onSwitchCamera={camera.switchCamera}
|
|
176
178
|
/>
|
|
177
|
-
|
|
179
|
+
|
|
180
|
+
<AlertDisplay alertTitle={faceCapture.alertTitle.value} />
|
|
181
|
+
|
|
182
|
+
{!faceCapture.isCapturing.value &&
|
|
183
|
+
!faceCapture.hasFinishedCapture.value && (
|
|
184
|
+
<CaptureControls
|
|
185
|
+
isCapturing={faceCapture.isCapturing.value}
|
|
186
|
+
hasFinishedCapture={faceCapture.hasFinishedCapture.value}
|
|
187
|
+
isReadyToCapture={faceCapture.isReadyToCapture.value}
|
|
188
|
+
captureButtonFallbackEnabled={
|
|
189
|
+
faceCapture.captureButtonFallbackEnabled.value
|
|
190
|
+
}
|
|
191
|
+
allowAgentMode={allowAgentMode}
|
|
192
|
+
agentSupported={camera.agentSupported}
|
|
193
|
+
showAgentModeForTests={showAgentModeForTests}
|
|
194
|
+
facingMode={camera.facingMode}
|
|
195
|
+
themeColor={themeColor}
|
|
196
|
+
onStartCapture={faceCapture.startCapture}
|
|
197
|
+
onSwitchCamera={camera.switchCamera}
|
|
198
|
+
/>
|
|
199
|
+
)}
|
|
200
|
+
</>
|
|
201
|
+
)}
|
|
178
202
|
|
|
179
203
|
{/* @ts-expect-error -- preact-custom-element doesn't have proper types for refs */}
|
|
180
204
|
{!hideAttribution && <powered-by-smile-id />}
|
|
@@ -227,6 +251,15 @@ const SmartSelfieCapture: FunctionComponent<Props> = ({
|
|
|
227
251
|
padding: 1rem;
|
|
228
252
|
font-family: sans-serif;
|
|
229
253
|
}
|
|
254
|
+
|
|
255
|
+
.camera-error {
|
|
256
|
+
margin-top: 1.5rem;
|
|
257
|
+
padding: 1rem 1.5rem;
|
|
258
|
+
color: #b00020;
|
|
259
|
+
text-align: center;
|
|
260
|
+
font-size: 1rem;
|
|
261
|
+
font-weight: 500;
|
|
262
|
+
}
|
|
230
263
|
`}</style>
|
|
231
264
|
</div>
|
|
232
265
|
);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useRef, useState, useEffect } from 'preact/hooks';
|
|
2
|
+
import SmartCamera from '../../../../../domain/camera/src/SmartCamera';
|
|
2
3
|
|
|
3
4
|
export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
4
5
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
5
6
|
const streamRef = useRef<MediaStream | null>(null);
|
|
6
7
|
const [facingMode, setFacingMode] = useState(initialFacingMode);
|
|
7
8
|
const [agentSupported, setAgentSupported] = useState(false);
|
|
9
|
+
const [cameraError, setCameraError] = useState<string | null>(null);
|
|
8
10
|
const onCameraSwitchCallbackRef = useRef<(() => void) | null>(null);
|
|
9
11
|
const isSwitchingCameraRef = useRef(false);
|
|
10
12
|
const timeoutIdsRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
|
@@ -63,6 +65,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
63
65
|
video: { facingMode: targetFacingMode || facingMode },
|
|
64
66
|
});
|
|
65
67
|
streamRef.current = stream;
|
|
68
|
+
setCameraError(null);
|
|
66
69
|
|
|
67
70
|
const track = stream.getVideoTracks()[0];
|
|
68
71
|
const settings = track.getSettings();
|
|
@@ -94,9 +97,15 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
94
97
|
}
|
|
95
98
|
} catch (error) {
|
|
96
99
|
console.error('Failed to start camera:', error);
|
|
100
|
+
setCameraError(SmartCamera.handleCameraError(error as Error));
|
|
101
|
+
throw error;
|
|
97
102
|
}
|
|
98
103
|
};
|
|
99
104
|
|
|
105
|
+
const retryCamera = async () => {
|
|
106
|
+
await startCamera(facingMode);
|
|
107
|
+
};
|
|
108
|
+
|
|
100
109
|
const switchCamera = async () => {
|
|
101
110
|
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
|
|
102
111
|
isSwitchingCameraRef.current = true;
|
|
@@ -118,6 +127,7 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
118
127
|
await startCamera(previousFacingMode);
|
|
119
128
|
} catch (restoreError) {
|
|
120
129
|
console.error('Failed to restore previous camera:', restoreError);
|
|
130
|
+
setCameraError(SmartCamera.handleCameraError(restoreError as Error));
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
};
|
|
@@ -229,7 +239,9 @@ export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
|
229
239
|
streamRef,
|
|
230
240
|
facingMode,
|
|
231
241
|
agentSupported,
|
|
242
|
+
cameraError,
|
|
232
243
|
startCamera,
|
|
244
|
+
retryCamera,
|
|
233
245
|
switchCamera,
|
|
234
246
|
checkAgentSupport,
|
|
235
247
|
stopCamera,
|
|
@@ -67,10 +67,61 @@ declare global {
|
|
|
67
67
|
instance: FaceLandmarker | null;
|
|
68
68
|
loading: Promise<FaceLandmarker> | null;
|
|
69
69
|
loaded: boolean;
|
|
70
|
+
supportsWasmReftypes?: boolean;
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @description Detects whether the current runtime supports the WebAssembly
|
|
77
|
+
* reference-types proposal (the `externref` value type). MediaPipe Tasks
|
|
78
|
+
* Vision ships a .wasm that uses `externref`; on older engines (e.g. Chrome
|
|
79
|
+
* < 96, Safari < 15, Firefox < 79) `WebAssembly.instantiate` throws
|
|
80
|
+
* `CompileError: invalid value type 'externref'`. We probe support once with
|
|
81
|
+
* `WebAssembly.validate` against a tiny module whose only feature is an
|
|
82
|
+
* `externref`-typed global so callers can short-circuit and fall back to the
|
|
83
|
+
* legacy selfie capture flow instead of triggering an unhandled rejection.
|
|
84
|
+
* @returns {boolean} True if the runtime accepts reftypes / externref.
|
|
85
|
+
*/
|
|
86
|
+
const supportsWasmReftypes = (): boolean => {
|
|
87
|
+
if (typeof WebAssembly === 'undefined' || !WebAssembly.validate) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Minimal module: magic + version + global section with one externref
|
|
93
|
+
// global (value type 0x6f) initialized to ref.null extern (0xd0 0x6f 0x0b).
|
|
94
|
+
const bytes = new Uint8Array([
|
|
95
|
+
0x00,
|
|
96
|
+
0x61,
|
|
97
|
+
0x73,
|
|
98
|
+
0x6d, // \0asm magic
|
|
99
|
+
0x01,
|
|
100
|
+
0x00,
|
|
101
|
+
0x00,
|
|
102
|
+
0x00, // version 1
|
|
103
|
+
0x06,
|
|
104
|
+
0x06,
|
|
105
|
+
0x01, // global section, 6 bytes, 1 global
|
|
106
|
+
0x6f,
|
|
107
|
+
0x00, // externref, immutable
|
|
108
|
+
0xd0,
|
|
109
|
+
0x6f,
|
|
110
|
+
0x0b, // ref.null extern; end
|
|
111
|
+
]);
|
|
112
|
+
return WebAssembly.validate(bytes);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export class UnsupportedMediapipeEnvironmentError extends Error {
|
|
119
|
+
constructor(message: string) {
|
|
120
|
+
super(message);
|
|
121
|
+
this.name = 'UnsupportedMediapipeEnvironmentError';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
74
125
|
/**
|
|
75
126
|
* @description Reads system architecture hints from User-Agent Client Hints.
|
|
76
127
|
* @returns {Promise<string | null>} Lower-cased hint string or null when hints are unavailable.
|
|
@@ -175,6 +226,19 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
|
175
226
|
return mediapipeGlobal.loading;
|
|
176
227
|
}
|
|
177
228
|
|
|
229
|
+
// Fail fast on engines that don't support WebAssembly reftypes/externref.
|
|
230
|
+
// The MediaPipe Tasks Vision .wasm uses externref globals; instantiating it
|
|
231
|
+
// on older browsers throws an unhandled `CompileError`. We detect once and
|
|
232
|
+
// cache the result so callers fall back to the legacy capture flow.
|
|
233
|
+
if (mediapipeGlobal.supportsWasmReftypes === undefined) {
|
|
234
|
+
mediapipeGlobal.supportsWasmReftypes = supportsWasmReftypes();
|
|
235
|
+
}
|
|
236
|
+
if (!mediapipeGlobal.supportsWasmReftypes) {
|
|
237
|
+
throw new UnsupportedMediapipeEnvironmentError(
|
|
238
|
+
'WebAssembly reference types (externref) are not supported in this browser; MediaPipe Tasks Vision cannot be loaded.',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
178
242
|
mediapipeGlobal.loading = (async () => {
|
|
179
243
|
try {
|
|
180
244
|
const vision = await FilesetResolver.forVisionTasks(
|
|
@@ -212,4 +276,5 @@ export const getMediapipeInstance = async (): Promise<FaceLandmarker> => {
|
|
|
212
276
|
export const __testUtils = {
|
|
213
277
|
matchesExcludedGpu,
|
|
214
278
|
getDelegateFromGpuDetection,
|
|
279
|
+
supportsWasmReftypes,
|
|
215
280
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smileid/web-components",
|
|
3
|
-
"version": "11.4.
|
|
3
|
+
"version": "11.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "dist/esm/main.js",
|
|
6
6
|
"module": "dist/esm/main.js",
|
|
@@ -76,6 +76,15 @@
|
|
|
76
76
|
],
|
|
77
77
|
"type": "module",
|
|
78
78
|
"author": "SmileID <support@usesmileid.com> (https://usesmileid.com)",
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/smileidentity/web-client.git",
|
|
82
|
+
"directory": "packages/web-components"
|
|
83
|
+
},
|
|
84
|
+
"bugs": {
|
|
85
|
+
"url": "https://github.com/smileidentity/web-client/issues"
|
|
86
|
+
},
|
|
87
|
+
"homepage": "https://github.com/smileidentity/web-client#readme",
|
|
79
88
|
"dependencies": {
|
|
80
89
|
"@lottiefiles/dotlottie-web": "^0.71.0",
|
|
81
90
|
"@mediapipe/tasks-vision": "^0.10.22-rc.20250304",
|