@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.
@@ -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
- try {
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
- try {
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
- this.modelLoadingPromise = this.loadModels();
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
- // initialize as no-op.
146
- let reject = () => { };
147
- let resolve = () => { };
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
- resolve({
150
- done: false,
151
- value: new Uint8Array(event.data),
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
- reject(error);
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
- reject(streamError);
185
+ endError = streamError;
165
186
  }
166
- else {
167
- resolve({
168
- done: true,
169
- value: undefined,
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
- resolve = _resolve;
178
- reject = _reject;
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, the notify the output stream to throw if there's
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
  }
@@ -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';
@@ -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';
@@ -1,3 +1,3 @@
1
- const VERSION = '3.6.5';
1
+ const VERSION = '3.6.7';
2
2
 
3
3
  export { VERSION };
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
- this.modelLoadingPromise = this.loadModels();
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.5';
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
- // initialize as no-op.
1376
- let reject = () => { };
1377
- let resolve = () => { };
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
- resolve({
1380
- done: false,
1381
- value: new Uint8Array(event.data),
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
- reject(error);
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
- reject(streamError);
1439
+ endError = streamError;
1395
1440
  }
1396
- else {
1397
- resolve({
1398
- done: true,
1399
- value: undefined,
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
- resolve = _resolve;
1408
- reject = _reject;
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, the notify the output stream to throw if there's
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
- try {
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
- try {
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
  /**
@@ -1 +1 @@
1
- export declare const VERSION = "3.6.5";
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.5",
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.0",
68
+ "uuid": "^11.1.1",
69
69
  "xstate": "^4.33.6"
70
70
  },
71
71
  "devDependencies": {