@aws-amplify/ui-react-liveness 3.6.5 → 3.6.7
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/components/FaceLivenessDetector/FaceLivenessDetectorCore.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/service/machine/machine.mjs +15 -15
- package/dist/esm/components/FaceLivenessDetector/service/types/faceDetection.mjs +27 -1
- package/dist/esm/components/FaceLivenessDetector/service/utils/createStreamingClient/CustomWebSocketFetchHandler.mjs +75 -19
- package/dist/esm/components/FaceLivenessDetector/service/utils/getFaceMatchStateInLivenessOval.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/service/utils/liveness.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/shared/FaceLivenessErrorModal.mjs +1 -1
- package/dist/esm/components/FaceLivenessDetector/shared/Hint.mjs +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +163 -83
- package/dist/types/components/FaceLivenessDetector/service/types/faceDetection.d.ts +3 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +2 -2
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { useInterpret } from '@xstate/react';
|
|
3
3
|
import { livenessMachine } from './service/machine/machine.mjs';
|
|
4
|
+
import './service/utils/constants.mjs';
|
|
4
5
|
import './service/types/liveness.mjs';
|
|
5
6
|
import '@tensorflow/tfjs-core';
|
|
6
7
|
import '../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-backend-wasm';
|
|
8
9
|
import '@tensorflow/tfjs-backend-cpu';
|
|
9
10
|
import '@aws-amplify/core/internals/utils';
|
|
10
|
-
import './service/utils/constants.mjs';
|
|
11
11
|
import './service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
12
12
|
import '@aws-amplify/ui';
|
|
13
13
|
import './service/utils/createStreamingClient/createStreamingClient.mjs';
|
|
@@ -3,6 +3,7 @@ import { classNames } from '@aws-amplify/ui';
|
|
|
3
3
|
import { View, Flex, Loader, Text, Button } from '@aws-amplify/ui-react';
|
|
4
4
|
import { useColorMode } from '@aws-amplify/ui-react/internal';
|
|
5
5
|
import '../service/machine/machine.mjs';
|
|
6
|
+
import { FACE_MOVEMENT_CHALLENGE } from '../service/utils/constants.mjs';
|
|
6
7
|
import { FaceMatchState } from '../service/types/liveness.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-core';
|
|
8
9
|
import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
|
|
@@ -10,7 +11,6 @@ import '@tensorflow/tfjs-backend-wasm';
|
|
|
10
11
|
import '@tensorflow/tfjs-backend-cpu';
|
|
11
12
|
import '@aws-amplify/core/internals/utils';
|
|
12
13
|
import { drawStaticOval, clearOvalCanvas } from '../service/utils/liveness.mjs';
|
|
13
|
-
import { FACE_MOVEMENT_CHALLENGE } from '../service/utils/constants.mjs';
|
|
14
14
|
import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
15
15
|
import '../service/utils/createStreamingClient/createStreamingClient.mjs';
|
|
16
16
|
import '../service/utils/StreamRecorder/StreamRecorder.mjs';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Flex, Text, Button, View } from '@aws-amplify/ui-react';
|
|
3
3
|
import '../service/machine/machine.mjs';
|
|
4
|
+
import '../service/utils/constants.mjs';
|
|
4
5
|
import '../service/types/liveness.mjs';
|
|
5
6
|
import { LivenessErrorState } from '../service/types/error.mjs';
|
|
6
7
|
import '@tensorflow/tfjs-core';
|
|
@@ -8,7 +9,6 @@ import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-de
|
|
|
8
9
|
import '@tensorflow/tfjs-backend-wasm';
|
|
9
10
|
import '@tensorflow/tfjs-backend-cpu';
|
|
10
11
|
import '@aws-amplify/core/internals/utils';
|
|
11
|
-
import '../service/utils/constants.mjs';
|
|
12
12
|
import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
13
13
|
import '@aws-amplify/ui';
|
|
14
14
|
import '../service/utils/createStreamingClient/createStreamingClient.mjs';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { v4 } from 'uuid';
|
|
2
2
|
import { createMachine, assign, actions, spawn } from 'xstate';
|
|
3
3
|
import { fillOverlayCanvasFractional, generateBboxFromLandmarks, getOvalBoundingBox, getIntersectionOverUnion, estimateIllumination, getOvalDetailsFromSessionInformation, drawLivenessOvalInCanvas, getStaticLivenessOvalDetails, isFaceDistanceBelowThreshold, getFaceMatchState, isCameraDeviceVirtual, getColorsSequencesFromSessionInformation } from '../utils/liveness.mjs';
|
|
4
|
+
import { FACE_MOVEMENT_AND_LIGHT_CHALLENGE, FACE_MOVEMENT_CHALLENGE, WS_CLOSURE_CODE } from '../utils/constants.mjs';
|
|
4
5
|
import { FaceMatchState } from '../types/liveness.mjs';
|
|
5
6
|
import { LivenessErrorState } from '../types/error.mjs';
|
|
6
7
|
import { BlazeFaceFaceDetection } from '../utils/blazefaceFaceDetection.mjs';
|
|
7
8
|
import { getFaceMatchStateInLivenessOval } from '../utils/getFaceMatchStateInLivenessOval.mjs';
|
|
8
9
|
import { ColorSequenceDisplay } from '../utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
9
|
-
import { FACE_MOVEMENT_AND_LIGHT_CHALLENGE, FACE_MOVEMENT_CHALLENGE, WS_CLOSURE_CODE } from '../utils/constants.mjs';
|
|
10
10
|
import { createRequestStreamGenerator } from '../utils/createRequestStreamGenerator/createRequestStreamGenerator.mjs';
|
|
11
11
|
import { createSessionEndEvent, getTrackDimensions, createColorDisplayEvent, createSessionStartEvent } from '../utils/createRequestStreamGenerator/utils.mjs';
|
|
12
12
|
import { createStreamingClient } from '../utils/createStreamingClient/createStreamingClient.mjs';
|
|
@@ -203,6 +203,10 @@ const livenessMachine = createMachine({
|
|
|
203
203
|
'spawnResponseStreamActor',
|
|
204
204
|
],
|
|
205
205
|
},
|
|
206
|
+
onError: {
|
|
207
|
+
target: '#livenessMachine.error',
|
|
208
|
+
actions: 'updateErrorStateForRuntime',
|
|
209
|
+
},
|
|
206
210
|
},
|
|
207
211
|
},
|
|
208
212
|
waitForSessionInfo: {
|
|
@@ -231,6 +235,10 @@ const livenessMachine = createMachine({
|
|
|
231
235
|
target: 'checkFaceDetectedBeforeStart',
|
|
232
236
|
actions: 'updateFaceMatchBeforeStartDetails',
|
|
233
237
|
},
|
|
238
|
+
onError: {
|
|
239
|
+
target: 'error',
|
|
240
|
+
actions: 'updateErrorStateForRuntime',
|
|
241
|
+
},
|
|
234
242
|
},
|
|
235
243
|
},
|
|
236
244
|
checkFaceDetectedBeforeStart: {
|
|
@@ -249,6 +257,10 @@ const livenessMachine = createMachine({
|
|
|
249
257
|
target: 'checkFaceDistanceBeforeRecording',
|
|
250
258
|
actions: 'updateFaceDistanceBeforeRecording',
|
|
251
259
|
},
|
|
260
|
+
onError: {
|
|
261
|
+
target: 'error',
|
|
262
|
+
actions: 'updateErrorStateForRuntime',
|
|
263
|
+
},
|
|
252
264
|
},
|
|
253
265
|
},
|
|
254
266
|
checkFaceDistanceBeforeRecording: {
|
|
@@ -926,13 +938,7 @@ const livenessMachine = createMachine({
|
|
|
926
938
|
const { videoEl } = context.videoAssociatedParams;
|
|
927
939
|
const { faceDetector } = context.ovalAssociatedParams;
|
|
928
940
|
// initialize models
|
|
929
|
-
|
|
930
|
-
await faceDetector.modelLoadingPromise;
|
|
931
|
-
}
|
|
932
|
-
catch (err) {
|
|
933
|
-
// eslint-disable-next-line no-console
|
|
934
|
-
console.log({ err });
|
|
935
|
-
}
|
|
941
|
+
await faceDetector.modelLoadingPromise;
|
|
936
942
|
// detect face
|
|
937
943
|
const faceMatchState = await getFaceMatchState(faceDetector, videoEl);
|
|
938
944
|
return { faceMatchState };
|
|
@@ -964,13 +970,7 @@ const livenessMachine = createMachine({
|
|
|
964
970
|
const { videoEl, canvasEl, isMobile } = context.videoAssociatedParams;
|
|
965
971
|
const { faceDetector } = context.ovalAssociatedParams;
|
|
966
972
|
// initialize models
|
|
967
|
-
|
|
968
|
-
await faceDetector.modelLoadingPromise;
|
|
969
|
-
}
|
|
970
|
-
catch (err) {
|
|
971
|
-
// eslint-disable-next-line no-console
|
|
972
|
-
console.log({ err });
|
|
973
|
-
}
|
|
973
|
+
await faceDetector.modelLoadingPromise;
|
|
974
974
|
// detect face
|
|
975
975
|
const detectedFaces = await faceDetector.detectFaces(videoEl);
|
|
976
976
|
let initialFace;
|
|
@@ -1,14 +1,40 @@
|
|
|
1
|
+
import { CONNECTION_TIMEOUT } from '../utils/constants.mjs';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The abstract class representing FaceDetection
|
|
3
5
|
* to be implemented for different libraries.
|
|
4
6
|
*/
|
|
5
7
|
class FaceDetection {
|
|
8
|
+
constructor() {
|
|
9
|
+
this._modelLoadTriggered = false;
|
|
10
|
+
}
|
|
6
11
|
/**
|
|
7
12
|
* Triggers the `loadModels` method and stores
|
|
8
13
|
* the corresponding promise to be awaited later.
|
|
14
|
+
* Applies a timeout to prevent indefinite hangs if the
|
|
15
|
+
* network request for model assets stalls.
|
|
9
16
|
*/
|
|
10
17
|
triggerModelLoading() {
|
|
11
|
-
|
|
18
|
+
if (this._modelLoadTriggered) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this._modelLoadTriggered = true;
|
|
22
|
+
let timeoutId;
|
|
23
|
+
this.modelLoadingPromise = Promise.race([
|
|
24
|
+
// `finally` ensures the timeout is cleared whether loadModels
|
|
25
|
+
// resolves or rejects, preventing a leaked timer on fast failures.
|
|
26
|
+
this.loadModels().finally(() => {
|
|
27
|
+
clearTimeout(timeoutId);
|
|
28
|
+
}),
|
|
29
|
+
new Promise((_, reject) => {
|
|
30
|
+
timeoutId = setTimeout(() => reject(new Error('Face detection model loading timed out. Check your network connection and try again.')),
|
|
31
|
+
// Uses the same 10s timeout as the WebSocket connection phase.
|
|
32
|
+
// The WASM binary (~425KB) + model (~100KB) should load well
|
|
33
|
+
// within 10s on any reasonable connection. If it hasn't loaded
|
|
34
|
+
// by then, the request is likely stalled (not just slow).
|
|
35
|
+
CONNECTION_TIMEOUT);
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
12
38
|
}
|
|
13
39
|
}
|
|
14
40
|
|
|
@@ -142,40 +142,97 @@ class CustomWebSocketFetchHandler {
|
|
|
142
142
|
let streamError = undefined;
|
|
143
143
|
// To notify onclose event that error has occurred.
|
|
144
144
|
let socketErrorOccurred = false;
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
// Buffer for messages that arrive before the consumer calls .next().
|
|
146
|
+
// This prevents dropped messages when the server responds faster than
|
|
147
|
+
// the client sets up its async iterator (race condition).
|
|
148
|
+
const messageQueue = [];
|
|
149
|
+
let waitingResolve = null;
|
|
150
|
+
let waitingReject = null;
|
|
151
|
+
let done = false;
|
|
152
|
+
let endError = undefined;
|
|
148
153
|
socket.onmessage = (event) => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
const chunk = new Uint8Array(event.data);
|
|
155
|
+
if (waitingResolve) {
|
|
156
|
+
// Consumer is waiting for data — deliver immediately
|
|
157
|
+
const resolve = waitingResolve;
|
|
158
|
+
waitingResolve = null;
|
|
159
|
+
waitingReject = null;
|
|
160
|
+
resolve({ done: false, value: chunk });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Consumer hasn't called .next() yet — buffer the message
|
|
164
|
+
messageQueue.push(chunk);
|
|
165
|
+
}
|
|
153
166
|
};
|
|
154
167
|
socket.onerror = (error) => {
|
|
155
168
|
socketErrorOccurred = true;
|
|
156
169
|
socket.close();
|
|
157
|
-
|
|
170
|
+
done = true;
|
|
171
|
+
endError = error;
|
|
172
|
+
if (waitingReject) {
|
|
173
|
+
const reject = waitingReject;
|
|
174
|
+
waitingResolve = null;
|
|
175
|
+
waitingReject = null;
|
|
176
|
+
reject(error);
|
|
177
|
+
}
|
|
158
178
|
};
|
|
159
|
-
socket.onclose = () => {
|
|
179
|
+
socket.onclose = (event) => {
|
|
160
180
|
this.removeNotUsableSockets(socket.url);
|
|
161
181
|
if (socketErrorOccurred)
|
|
162
182
|
return;
|
|
183
|
+
done = true;
|
|
163
184
|
if (streamError) {
|
|
164
|
-
|
|
185
|
+
endError = streamError;
|
|
165
186
|
}
|
|
166
|
-
else
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
187
|
+
else if (event.code !== WS_CLOSURE_CODE.SUCCESS_CODE &&
|
|
188
|
+
event.code !== 1001) {
|
|
189
|
+
// Server closed the connection with an abnormal code (e.g. 4001
|
|
190
|
+
// StreamIdleTimeout, 4003 SessionExpired, 1006 abnormal closure).
|
|
191
|
+
endError = new Error(`Server ended the connection unexpectedly (code ${event.code}` +
|
|
192
|
+
(event.reason ? `: ${event.reason}` : '') +
|
|
193
|
+
')');
|
|
194
|
+
}
|
|
195
|
+
if (waitingResolve) {
|
|
196
|
+
if (endError) {
|
|
197
|
+
const reject = waitingReject;
|
|
198
|
+
waitingResolve = null;
|
|
199
|
+
waitingReject = null;
|
|
200
|
+
reject(endError);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const resolve = waitingResolve;
|
|
204
|
+
waitingResolve = null;
|
|
205
|
+
waitingReject = null;
|
|
206
|
+
resolve({ done: true, value: undefined });
|
|
207
|
+
}
|
|
171
208
|
}
|
|
172
209
|
};
|
|
173
210
|
const outputStream = {
|
|
174
211
|
[Symbol.asyncIterator]: () => ({
|
|
175
212
|
next: () => {
|
|
213
|
+
// If there are buffered messages, deliver the next one immediately
|
|
214
|
+
if (messageQueue.length > 0) {
|
|
215
|
+
return Promise.resolve({
|
|
216
|
+
done: false,
|
|
217
|
+
value: messageQueue.shift(),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// If the stream has ended, resolve/reject accordingly
|
|
221
|
+
if (done) {
|
|
222
|
+
if (endError) {
|
|
223
|
+
return Promise.reject(endError instanceof Error
|
|
224
|
+
? endError
|
|
225
|
+
: new Error('Stream ended with an error'));
|
|
226
|
+
}
|
|
227
|
+
return Promise.resolve({
|
|
228
|
+
done: true,
|
|
229
|
+
value: undefined,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// Otherwise wait for the next message or close event
|
|
176
233
|
return new Promise((_resolve, _reject) => {
|
|
177
|
-
|
|
178
|
-
|
|
234
|
+
waitingResolve = _resolve;
|
|
235
|
+
waitingReject = _reject;
|
|
179
236
|
});
|
|
180
237
|
},
|
|
181
238
|
}),
|
|
@@ -198,8 +255,7 @@ class CustomWebSocketFetchHandler {
|
|
|
198
255
|
catch (err) {
|
|
199
256
|
// We don't throw the error here because the send()'s returned
|
|
200
257
|
// would already be settled by the time sending chunk throws error.
|
|
201
|
-
// Instead,
|
|
202
|
-
// exceptions
|
|
258
|
+
// Instead, notify the output stream to throw if there's exceptions
|
|
203
259
|
if (err instanceof Error) {
|
|
204
260
|
streamError = err;
|
|
205
261
|
}
|
package/dist/esm/components/FaceLivenessDetector/service/utils/getFaceMatchStateInLivenessOval.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { FACE_MATCH_RANGE_MAX, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, FACE_MATCH_RANGE_MIN } from './constants.mjs';
|
|
1
2
|
import { FaceMatchState } from '../types/liveness.mjs';
|
|
2
3
|
import { generateBboxFromLandmarks, getOvalBoundingBox, getIntersectionOverUnion } from './liveness.mjs';
|
|
3
|
-
import { FACE_MATCH_RANGE_MAX, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, FACE_MATCH_RANGE_MIN } from './constants.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Returns the state of the provided face with respect to the provided liveness oval.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { OVAL_HEIGHT_WIDTH_RATIO, PUPIL_DISTANCE_WEIGHT, FACE_HEIGHT_WEIGHT } from './constants.mjs';
|
|
1
2
|
import { IlluminationState, FaceMatchState } from '../types/liveness.mjs';
|
|
2
3
|
import { LivenessErrorState } from '../types/error.mjs';
|
|
3
|
-
import { OVAL_HEIGHT_WIDTH_RATIO, PUPIL_DISTANCE_WEIGHT, FACE_HEIGHT_WEIGHT } from './constants.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Returns the random number between min and max
|
|
@@ -3,13 +3,13 @@ import { ComponentClassName } from '@aws-amplify/ui';
|
|
|
3
3
|
import { Flex, View } from '@aws-amplify/ui-react';
|
|
4
4
|
import { CancelButton } from './CancelButton.mjs';
|
|
5
5
|
import '../service/machine/machine.mjs';
|
|
6
|
+
import '../service/utils/constants.mjs';
|
|
6
7
|
import '../service/types/liveness.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-core';
|
|
8
9
|
import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
|
|
9
10
|
import '@tensorflow/tfjs-backend-wasm';
|
|
10
11
|
import '@tensorflow/tfjs-backend-cpu';
|
|
11
12
|
import '@aws-amplify/core/internals/utils';
|
|
12
|
-
import '../service/utils/constants.mjs';
|
|
13
13
|
import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
14
14
|
import '../service/utils/createStreamingClient/createStreamingClient.mjs';
|
|
15
15
|
import '../service/utils/StreamRecorder/StreamRecorder.mjs';
|
|
@@ -2,6 +2,7 @@ import React__default from 'react';
|
|
|
2
2
|
import { Flex, Button, Text } from '@aws-amplify/ui-react';
|
|
3
3
|
import { AlertIcon } from '@aws-amplify/ui-react/internal';
|
|
4
4
|
import '../service/machine/machine.mjs';
|
|
5
|
+
import '../service/utils/constants.mjs';
|
|
5
6
|
import '../service/types/liveness.mjs';
|
|
6
7
|
import { LivenessErrorState } from '../service/types/error.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-core';
|
|
@@ -9,7 +10,6 @@ import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-de
|
|
|
9
10
|
import '@tensorflow/tfjs-backend-wasm';
|
|
10
11
|
import '@tensorflow/tfjs-backend-cpu';
|
|
11
12
|
import '@aws-amplify/core/internals/utils';
|
|
12
|
-
import '../service/utils/constants.mjs';
|
|
13
13
|
import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
14
14
|
import '@aws-amplify/ui';
|
|
15
15
|
import '../service/utils/createStreamingClient/createStreamingClient.mjs';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { VisuallyHidden, View } from '@aws-amplify/ui-react';
|
|
3
3
|
import '../service/machine/machine.mjs';
|
|
4
|
+
import '../service/utils/constants.mjs';
|
|
4
5
|
import { FaceMatchState, IlluminationState } from '../service/types/liveness.mjs';
|
|
5
6
|
import '@tensorflow/tfjs-core';
|
|
6
7
|
import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-backend-wasm';
|
|
8
9
|
import '@tensorflow/tfjs-backend-cpu';
|
|
9
10
|
import '@aws-amplify/core/internals/utils';
|
|
10
|
-
import '../service/utils/constants.mjs';
|
|
11
11
|
import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
12
12
|
import '@aws-amplify/ui';
|
|
13
13
|
import '../service/utils/createStreamingClient/createStreamingClient.mjs';
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { default as FaceLivenessDetector } from './components/FaceLivenessDetector/FaceLivenessDetector.mjs';
|
|
2
2
|
export { default as FaceLivenessDetectorCore } from './components/FaceLivenessDetector/FaceLivenessDetectorCore.mjs';
|
|
3
3
|
import './components/FaceLivenessDetector/service/machine/machine.mjs';
|
|
4
|
+
import './components/FaceLivenessDetector/service/utils/constants.mjs';
|
|
4
5
|
import './components/FaceLivenessDetector/service/types/liveness.mjs';
|
|
5
6
|
import '@tensorflow/tfjs-core';
|
|
6
7
|
import './packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
|
|
7
8
|
import '@tensorflow/tfjs-backend-wasm';
|
|
8
9
|
import '@tensorflow/tfjs-backend-cpu';
|
|
9
10
|
import '@aws-amplify/core/internals/utils';
|
|
10
|
-
import './components/FaceLivenessDetector/service/utils/constants.mjs';
|
|
11
11
|
import './components/FaceLivenessDetector/service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
|
|
12
12
|
import '@aws-amplify/ui';
|
|
13
13
|
import './components/FaceLivenessDetector/service/utils/createStreamingClient/createStreamingClient.mjs';
|
package/dist/esm/version.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -43,17 +43,89 @@ function _interopNamespace(e) {
|
|
|
43
43
|
|
|
44
44
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
45
45
|
|
|
46
|
+
// Face distance is calculated as pupilDistance / ovalWidth.
|
|
47
|
+
// The further away you are from the camera the distance between your pupils will decrease, thus lowering the threshold values.
|
|
48
|
+
// Constants from science team to determine ocular distance (space between eyes)
|
|
49
|
+
const PUPIL_DISTANCE_WEIGHT = 2.0;
|
|
50
|
+
const FACE_HEIGHT_WEIGHT = 1.8;
|
|
51
|
+
// Constants from science team to find face match percentage
|
|
52
|
+
const FACE_MATCH_RANGE_MIN = 0;
|
|
53
|
+
const FACE_MATCH_RANGE_MAX = 1;
|
|
54
|
+
const FACE_MATCH_WEIGHT_MIN = 0.25;
|
|
55
|
+
const FACE_MATCH_WEIGHT_MAX = 0.75;
|
|
56
|
+
const OVAL_HEIGHT_WIDTH_RATIO = 1.618;
|
|
57
|
+
const WS_CLOSURE_CODE = {
|
|
58
|
+
SUCCESS_CODE: 1000,
|
|
59
|
+
DEFAULT_ERROR_CODE: 4000,
|
|
60
|
+
FACE_FIT_TIMEOUT: 4001,
|
|
61
|
+
USER_CANCEL: 4003,
|
|
62
|
+
RUNTIME_ERROR: 4005,
|
|
63
|
+
USER_ERROR_DURING_CONNECTION: 4007,
|
|
64
|
+
};
|
|
65
|
+
// number in milliseconds to record into each video chunk.
|
|
66
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start#timeslice
|
|
67
|
+
const TIME_SLICE = 1000;
|
|
68
|
+
// in MS, the rate at which colors are rendered/checked
|
|
69
|
+
const TICK_RATE = 10;
|
|
70
|
+
/**
|
|
71
|
+
* The number of seconds before the presigned URL expires.
|
|
72
|
+
* Used to override aws sdk default value of 60
|
|
73
|
+
*/
|
|
74
|
+
const REQUEST_EXPIRY = 299;
|
|
75
|
+
/**
|
|
76
|
+
* The maximum time in milliseconds that the connection phase of a request
|
|
77
|
+
* may take before the connection attempt is abandoned.
|
|
78
|
+
*/
|
|
79
|
+
const CONNECTION_TIMEOUT = 10000;
|
|
80
|
+
const FACE_MOVEMENT_AND_LIGHT_CHALLENGE = {
|
|
81
|
+
type: 'FaceMovementAndLightChallenge',
|
|
82
|
+
version: '2.0.0',
|
|
83
|
+
};
|
|
84
|
+
const FACE_MOVEMENT_CHALLENGE = {
|
|
85
|
+
type: 'FaceMovementChallenge',
|
|
86
|
+
version: '1.0.0',
|
|
87
|
+
};
|
|
88
|
+
const SUPPORTED_CHALLENGES = [
|
|
89
|
+
FACE_MOVEMENT_AND_LIGHT_CHALLENGE,
|
|
90
|
+
FACE_MOVEMENT_CHALLENGE,
|
|
91
|
+
];
|
|
92
|
+
const queryParameterString = SUPPORTED_CHALLENGES.map((challenge) => `${challenge.type}_${challenge.version}`).join(',');
|
|
93
|
+
|
|
46
94
|
/**
|
|
47
95
|
* The abstract class representing FaceDetection
|
|
48
96
|
* to be implemented for different libraries.
|
|
49
97
|
*/
|
|
50
98
|
let FaceDetection$1 = class FaceDetection {
|
|
99
|
+
constructor() {
|
|
100
|
+
this._modelLoadTriggered = false;
|
|
101
|
+
}
|
|
51
102
|
/**
|
|
52
103
|
* Triggers the `loadModels` method and stores
|
|
53
104
|
* the corresponding promise to be awaited later.
|
|
105
|
+
* Applies a timeout to prevent indefinite hangs if the
|
|
106
|
+
* network request for model assets stalls.
|
|
54
107
|
*/
|
|
55
108
|
triggerModelLoading() {
|
|
56
|
-
|
|
109
|
+
if (this._modelLoadTriggered) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this._modelLoadTriggered = true;
|
|
113
|
+
let timeoutId;
|
|
114
|
+
this.modelLoadingPromise = Promise.race([
|
|
115
|
+
// `finally` ensures the timeout is cleared whether loadModels
|
|
116
|
+
// resolves or rejects, preventing a leaked timer on fast failures.
|
|
117
|
+
this.loadModels().finally(() => {
|
|
118
|
+
clearTimeout(timeoutId);
|
|
119
|
+
}),
|
|
120
|
+
new Promise((_, reject) => {
|
|
121
|
+
timeoutId = setTimeout(() => reject(new Error('Face detection model loading timed out. Check your network connection and try again.')),
|
|
122
|
+
// Uses the same 10s timeout as the WebSocket connection phase.
|
|
123
|
+
// The WASM binary (~425KB) + model (~100KB) should load well
|
|
124
|
+
// within 10s on any reasonable connection. If it hasn't loaded
|
|
125
|
+
// by then, the request is likely stalled (not just slow).
|
|
126
|
+
CONNECTION_TIMEOUT);
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
57
129
|
}
|
|
58
130
|
};
|
|
59
131
|
|
|
@@ -95,54 +167,6 @@ const LivenessErrorState = {
|
|
|
95
167
|
DEFAULT_CAMERA_NOT_FOUND_ERROR: 'DEFAULT_CAMERA_NOT_FOUND_ERROR',
|
|
96
168
|
};
|
|
97
169
|
|
|
98
|
-
// Face distance is calculated as pupilDistance / ovalWidth.
|
|
99
|
-
// The further away you are from the camera the distance between your pupils will decrease, thus lowering the threshold values.
|
|
100
|
-
// Constants from science team to determine ocular distance (space between eyes)
|
|
101
|
-
const PUPIL_DISTANCE_WEIGHT = 2.0;
|
|
102
|
-
const FACE_HEIGHT_WEIGHT = 1.8;
|
|
103
|
-
// Constants from science team to find face match percentage
|
|
104
|
-
const FACE_MATCH_RANGE_MIN = 0;
|
|
105
|
-
const FACE_MATCH_RANGE_MAX = 1;
|
|
106
|
-
const FACE_MATCH_WEIGHT_MIN = 0.25;
|
|
107
|
-
const FACE_MATCH_WEIGHT_MAX = 0.75;
|
|
108
|
-
const OVAL_HEIGHT_WIDTH_RATIO = 1.618;
|
|
109
|
-
const WS_CLOSURE_CODE = {
|
|
110
|
-
SUCCESS_CODE: 1000,
|
|
111
|
-
DEFAULT_ERROR_CODE: 4000,
|
|
112
|
-
FACE_FIT_TIMEOUT: 4001,
|
|
113
|
-
USER_CANCEL: 4003,
|
|
114
|
-
RUNTIME_ERROR: 4005,
|
|
115
|
-
USER_ERROR_DURING_CONNECTION: 4007,
|
|
116
|
-
};
|
|
117
|
-
// number in milliseconds to record into each video chunk.
|
|
118
|
-
// see https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start#timeslice
|
|
119
|
-
const TIME_SLICE = 1000;
|
|
120
|
-
// in MS, the rate at which colors are rendered/checked
|
|
121
|
-
const TICK_RATE = 10;
|
|
122
|
-
/**
|
|
123
|
-
* The number of seconds before the presigned URL expires.
|
|
124
|
-
* Used to override aws sdk default value of 60
|
|
125
|
-
*/
|
|
126
|
-
const REQUEST_EXPIRY = 299;
|
|
127
|
-
/**
|
|
128
|
-
* The maximum time in milliseconds that the connection phase of a request
|
|
129
|
-
* may take before the connection attempt is abandoned.
|
|
130
|
-
*/
|
|
131
|
-
const CONNECTION_TIMEOUT = 10000;
|
|
132
|
-
const FACE_MOVEMENT_AND_LIGHT_CHALLENGE = {
|
|
133
|
-
type: 'FaceMovementAndLightChallenge',
|
|
134
|
-
version: '2.0.0',
|
|
135
|
-
};
|
|
136
|
-
const FACE_MOVEMENT_CHALLENGE = {
|
|
137
|
-
type: 'FaceMovementChallenge',
|
|
138
|
-
version: '1.0.0',
|
|
139
|
-
};
|
|
140
|
-
const SUPPORTED_CHALLENGES = [
|
|
141
|
-
FACE_MOVEMENT_AND_LIGHT_CHALLENGE,
|
|
142
|
-
FACE_MOVEMENT_CHALLENGE,
|
|
143
|
-
];
|
|
144
|
-
const queryParameterString = SUPPORTED_CHALLENGES.map((challenge) => `${challenge.type}_${challenge.version}`).join(',');
|
|
145
|
-
|
|
146
170
|
/**
|
|
147
171
|
* Returns the random number between min and max
|
|
148
172
|
* seeded with the provided random seed.
|
|
@@ -1227,7 +1251,7 @@ function createRequestStreamGenerator(stream) {
|
|
|
1227
1251
|
};
|
|
1228
1252
|
}
|
|
1229
1253
|
|
|
1230
|
-
const VERSION = '3.6.
|
|
1254
|
+
const VERSION = '3.6.7';
|
|
1231
1255
|
|
|
1232
1256
|
const BASE_USER_AGENT = `ui-react-liveness/${VERSION}`;
|
|
1233
1257
|
const getLivenessUserAgent = () => {
|
|
@@ -1372,40 +1396,97 @@ class CustomWebSocketFetchHandler {
|
|
|
1372
1396
|
let streamError = undefined;
|
|
1373
1397
|
// To notify onclose event that error has occurred.
|
|
1374
1398
|
let socketErrorOccurred = false;
|
|
1375
|
-
//
|
|
1376
|
-
|
|
1377
|
-
|
|
1399
|
+
// Buffer for messages that arrive before the consumer calls .next().
|
|
1400
|
+
// This prevents dropped messages when the server responds faster than
|
|
1401
|
+
// the client sets up its async iterator (race condition).
|
|
1402
|
+
const messageQueue = [];
|
|
1403
|
+
let waitingResolve = null;
|
|
1404
|
+
let waitingReject = null;
|
|
1405
|
+
let done = false;
|
|
1406
|
+
let endError = undefined;
|
|
1378
1407
|
socket.onmessage = (event) => {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1408
|
+
const chunk = new Uint8Array(event.data);
|
|
1409
|
+
if (waitingResolve) {
|
|
1410
|
+
// Consumer is waiting for data — deliver immediately
|
|
1411
|
+
const resolve = waitingResolve;
|
|
1412
|
+
waitingResolve = null;
|
|
1413
|
+
waitingReject = null;
|
|
1414
|
+
resolve({ done: false, value: chunk });
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
// Consumer hasn't called .next() yet — buffer the message
|
|
1418
|
+
messageQueue.push(chunk);
|
|
1419
|
+
}
|
|
1383
1420
|
};
|
|
1384
1421
|
socket.onerror = (error) => {
|
|
1385
1422
|
socketErrorOccurred = true;
|
|
1386
1423
|
socket.close();
|
|
1387
|
-
|
|
1424
|
+
done = true;
|
|
1425
|
+
endError = error;
|
|
1426
|
+
if (waitingReject) {
|
|
1427
|
+
const reject = waitingReject;
|
|
1428
|
+
waitingResolve = null;
|
|
1429
|
+
waitingReject = null;
|
|
1430
|
+
reject(error);
|
|
1431
|
+
}
|
|
1388
1432
|
};
|
|
1389
|
-
socket.onclose = () => {
|
|
1433
|
+
socket.onclose = (event) => {
|
|
1390
1434
|
this.removeNotUsableSockets(socket.url);
|
|
1391
1435
|
if (socketErrorOccurred)
|
|
1392
1436
|
return;
|
|
1437
|
+
done = true;
|
|
1393
1438
|
if (streamError) {
|
|
1394
|
-
|
|
1439
|
+
endError = streamError;
|
|
1395
1440
|
}
|
|
1396
|
-
else
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1441
|
+
else if (event.code !== WS_CLOSURE_CODE.SUCCESS_CODE &&
|
|
1442
|
+
event.code !== 1001) {
|
|
1443
|
+
// Server closed the connection with an abnormal code (e.g. 4001
|
|
1444
|
+
// StreamIdleTimeout, 4003 SessionExpired, 1006 abnormal closure).
|
|
1445
|
+
endError = new Error(`Server ended the connection unexpectedly (code ${event.code}` +
|
|
1446
|
+
(event.reason ? `: ${event.reason}` : '') +
|
|
1447
|
+
')');
|
|
1448
|
+
}
|
|
1449
|
+
if (waitingResolve) {
|
|
1450
|
+
if (endError) {
|
|
1451
|
+
const reject = waitingReject;
|
|
1452
|
+
waitingResolve = null;
|
|
1453
|
+
waitingReject = null;
|
|
1454
|
+
reject(endError);
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
const resolve = waitingResolve;
|
|
1458
|
+
waitingResolve = null;
|
|
1459
|
+
waitingReject = null;
|
|
1460
|
+
resolve({ done: true, value: undefined });
|
|
1461
|
+
}
|
|
1401
1462
|
}
|
|
1402
1463
|
};
|
|
1403
1464
|
const outputStream = {
|
|
1404
1465
|
[Symbol.asyncIterator]: () => ({
|
|
1405
1466
|
next: () => {
|
|
1467
|
+
// If there are buffered messages, deliver the next one immediately
|
|
1468
|
+
if (messageQueue.length > 0) {
|
|
1469
|
+
return Promise.resolve({
|
|
1470
|
+
done: false,
|
|
1471
|
+
value: messageQueue.shift(),
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
// If the stream has ended, resolve/reject accordingly
|
|
1475
|
+
if (done) {
|
|
1476
|
+
if (endError) {
|
|
1477
|
+
return Promise.reject(endError instanceof Error
|
|
1478
|
+
? endError
|
|
1479
|
+
: new Error('Stream ended with an error'));
|
|
1480
|
+
}
|
|
1481
|
+
return Promise.resolve({
|
|
1482
|
+
done: true,
|
|
1483
|
+
value: undefined,
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
// Otherwise wait for the next message or close event
|
|
1406
1487
|
return new Promise((_resolve, _reject) => {
|
|
1407
|
-
|
|
1408
|
-
|
|
1488
|
+
waitingResolve = _resolve;
|
|
1489
|
+
waitingReject = _reject;
|
|
1409
1490
|
});
|
|
1410
1491
|
},
|
|
1411
1492
|
}),
|
|
@@ -1428,8 +1509,7 @@ class CustomWebSocketFetchHandler {
|
|
|
1428
1509
|
catch (err) {
|
|
1429
1510
|
// We don't throw the error here because the send()'s returned
|
|
1430
1511
|
// would already be settled by the time sending chunk throws error.
|
|
1431
|
-
// Instead,
|
|
1432
|
-
// exceptions
|
|
1512
|
+
// Instead, notify the output stream to throw if there's exceptions
|
|
1433
1513
|
if (err instanceof Error) {
|
|
1434
1514
|
streamError = err;
|
|
1435
1515
|
}
|
|
@@ -1965,6 +2045,10 @@ const livenessMachine = xstate.createMachine({
|
|
|
1965
2045
|
'spawnResponseStreamActor',
|
|
1966
2046
|
],
|
|
1967
2047
|
},
|
|
2048
|
+
onError: {
|
|
2049
|
+
target: '#livenessMachine.error',
|
|
2050
|
+
actions: 'updateErrorStateForRuntime',
|
|
2051
|
+
},
|
|
1968
2052
|
},
|
|
1969
2053
|
},
|
|
1970
2054
|
waitForSessionInfo: {
|
|
@@ -1993,6 +2077,10 @@ const livenessMachine = xstate.createMachine({
|
|
|
1993
2077
|
target: 'checkFaceDetectedBeforeStart',
|
|
1994
2078
|
actions: 'updateFaceMatchBeforeStartDetails',
|
|
1995
2079
|
},
|
|
2080
|
+
onError: {
|
|
2081
|
+
target: 'error',
|
|
2082
|
+
actions: 'updateErrorStateForRuntime',
|
|
2083
|
+
},
|
|
1996
2084
|
},
|
|
1997
2085
|
},
|
|
1998
2086
|
checkFaceDetectedBeforeStart: {
|
|
@@ -2011,6 +2099,10 @@ const livenessMachine = xstate.createMachine({
|
|
|
2011
2099
|
target: 'checkFaceDistanceBeforeRecording',
|
|
2012
2100
|
actions: 'updateFaceDistanceBeforeRecording',
|
|
2013
2101
|
},
|
|
2102
|
+
onError: {
|
|
2103
|
+
target: 'error',
|
|
2104
|
+
actions: 'updateErrorStateForRuntime',
|
|
2105
|
+
},
|
|
2014
2106
|
},
|
|
2015
2107
|
},
|
|
2016
2108
|
checkFaceDistanceBeforeRecording: {
|
|
@@ -2688,13 +2780,7 @@ const livenessMachine = xstate.createMachine({
|
|
|
2688
2780
|
const { videoEl } = context.videoAssociatedParams;
|
|
2689
2781
|
const { faceDetector } = context.ovalAssociatedParams;
|
|
2690
2782
|
// initialize models
|
|
2691
|
-
|
|
2692
|
-
await faceDetector.modelLoadingPromise;
|
|
2693
|
-
}
|
|
2694
|
-
catch (err) {
|
|
2695
|
-
// eslint-disable-next-line no-console
|
|
2696
|
-
console.log({ err });
|
|
2697
|
-
}
|
|
2783
|
+
await faceDetector.modelLoadingPromise;
|
|
2698
2784
|
// detect face
|
|
2699
2785
|
const faceMatchState = await getFaceMatchState(faceDetector, videoEl);
|
|
2700
2786
|
return { faceMatchState };
|
|
@@ -2726,13 +2812,7 @@ const livenessMachine = xstate.createMachine({
|
|
|
2726
2812
|
const { videoEl, canvasEl, isMobile } = context.videoAssociatedParams;
|
|
2727
2813
|
const { faceDetector } = context.ovalAssociatedParams;
|
|
2728
2814
|
// initialize models
|
|
2729
|
-
|
|
2730
|
-
await faceDetector.modelLoadingPromise;
|
|
2731
|
-
}
|
|
2732
|
-
catch (err) {
|
|
2733
|
-
// eslint-disable-next-line no-console
|
|
2734
|
-
console.log({ err });
|
|
2735
|
-
}
|
|
2815
|
+
await faceDetector.modelLoadingPromise;
|
|
2736
2816
|
// detect face
|
|
2737
2817
|
const detectedFaces = await faceDetector.detectFaces(videoEl);
|
|
2738
2818
|
let initialFace;
|
|
@@ -22,9 +22,12 @@ export type Coordinate = [number, number];
|
|
|
22
22
|
*/
|
|
23
23
|
export declare abstract class FaceDetection {
|
|
24
24
|
modelLoadingPromise: Promise<void>;
|
|
25
|
+
private _modelLoadTriggered;
|
|
25
26
|
/**
|
|
26
27
|
* Triggers the `loadModels` method and stores
|
|
27
28
|
* the corresponding promise to be awaited later.
|
|
29
|
+
* Applies a timeout to prevent indefinite hangs if the
|
|
30
|
+
* network request for model assets stalls.
|
|
28
31
|
*/
|
|
29
32
|
triggerModelLoading(): void;
|
|
30
33
|
/**
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "3.6.
|
|
1
|
+
export declare const VERSION = "3.6.7";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-amplify/ui-react-liveness",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.7",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/esm/index.mjs",
|
|
6
6
|
"exports": {
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@tensorflow/tfjs-core": "4.11.0",
|
|
66
66
|
"@xstate/react": "^3.2.2",
|
|
67
67
|
"tslib": "^2.5.2",
|
|
68
|
-
"uuid": "^11.1.
|
|
68
|
+
"uuid": "^11.1.1",
|
|
69
69
|
"xstate": "^4.33.6"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|