@aws-amplify/ui-react-liveness 3.6.9 → 3.7.0
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 +18 -1
- package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.mjs +96 -6
- package/dist/esm/components/FaceLivenessDetector/LivenessCheck/LivenessCheck.mjs +2 -2
- package/dist/esm/components/FaceLivenessDetector/service/machine/machine.mjs +45 -22
- package/dist/esm/components/FaceLivenessDetector/service/utils/closeLivenessStream.mjs +28 -0
- package/dist/esm/components/FaceLivenessDetector/service/utils/liveness.mjs +90 -17
- package/dist/esm/components/FaceLivenessDetector/shared/CancelButton.mjs +4 -1
- package/dist/esm/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.mjs +2 -2
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +278 -47
- package/dist/styles.css +22 -0
- package/dist/types/components/FaceLivenessDetector/LivenessCheck/LivenessCameraModule.d.ts +1 -0
- package/dist/types/components/FaceLivenessDetector/service/types/machine.d.ts +1 -1
- package/dist/types/components/FaceLivenessDetector/service/utils/closeLivenessStream.d.ts +9 -0
- package/dist/types/components/FaceLivenessDetector/service/utils/index.d.ts +1 -0
- package/dist/types/components/FaceLivenessDetector/service/utils/liveness.d.ts +30 -0
- package/dist/types/components/FaceLivenessDetector/shared/CancelButton.d.ts +15 -0
- package/dist/types/components/FaceLivenessDetector/shared/DefaultStartScreenComponents.d.ts +11 -2
- package/dist/types/version.d.ts +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -256,12 +256,59 @@ function getStaticLivenessOvalDetails({ width, height, widthSeed = 1.0, centerXS
|
|
|
256
256
|
height: Math.floor(ovalHeight),
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Returns how much the intrinsic video frame is scaled inside the rendered
|
|
261
|
+
* video element. The video element uses the default `object-fit: contain`,
|
|
262
|
+
* so when the host container gives it a box that does not match the camera
|
|
263
|
+
* aspect ratio the frame is letterboxed and the smaller ratio applies.
|
|
264
|
+
*/
|
|
265
|
+
function getVideoScaleFactor(videoEl) {
|
|
266
|
+
// layout size, unaffected by CSS transforms such as a modal zoom animation
|
|
267
|
+
const { clientWidth: width, clientHeight: height } = videoEl;
|
|
268
|
+
const { videoWidth, videoHeight } = videoEl;
|
|
269
|
+
if (!videoWidth || !videoHeight) {
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
return Math.min(width / videoWidth, height / videoHeight);
|
|
273
|
+
}
|
|
274
|
+
// When filling a container taller than the camera frame, the frame is scaled
|
|
275
|
+
// up and cropped on the sides. Keep at least this fraction of the frame width
|
|
276
|
+
// visible so the liveness oval (centered, ~45% of the frame width) is never
|
|
277
|
+
// cropped. The frame is never cropped vertically since the oval spans nearly
|
|
278
|
+
// the full frame height.
|
|
279
|
+
const MIN_VISIBLE_FRAME_WIDTH_FRACTION = 0.7;
|
|
280
|
+
/**
|
|
281
|
+
* Returns the size and position of the video element so the camera frame
|
|
282
|
+
* fills as much of its container as possible without cropping the oval.
|
|
283
|
+
*/
|
|
284
|
+
function getVideoFillLayout({ containerWidth, containerHeight, videoWidth, videoHeight, }) {
|
|
285
|
+
const scale = Math.min(containerHeight / videoHeight, containerWidth / (videoWidth * MIN_VISIBLE_FRAME_WIDTH_FRACTION));
|
|
286
|
+
const width = videoWidth * scale;
|
|
287
|
+
const height = videoHeight * scale;
|
|
288
|
+
return {
|
|
289
|
+
width,
|
|
290
|
+
height,
|
|
291
|
+
left: (containerWidth - width) / 2,
|
|
292
|
+
top: (containerHeight - height) / 2,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Sizes the canvas drawing buffer to match its parent's box: the full screen
|
|
297
|
+
* on mobile and the video anchor box otherwise. The anchor can differ from the
|
|
298
|
+
* video element's box, which is centered in it and may be letterboxed or
|
|
299
|
+
* cropped on the sides.
|
|
300
|
+
*/
|
|
301
|
+
function resizeCanvasToDisplaySize(canvas) {
|
|
302
|
+
const { clientWidth, clientHeight } = canvas.parentElement ?? canvas;
|
|
303
|
+
canvas.width = Math.ceil(clientWidth);
|
|
304
|
+
canvas.height = Math.ceil(clientHeight);
|
|
305
|
+
}
|
|
259
306
|
/**
|
|
260
307
|
* Draws the provided liveness oval on the canvas.
|
|
261
308
|
*/
|
|
262
309
|
function drawLivenessOvalInCanvas({ canvas, oval, scaleFactor, videoEl, isStartScreen, }) {
|
|
263
310
|
const { flippedCenterX, centerY, width, height } = oval;
|
|
264
|
-
const { width: canvasWidth, height: canvasHeight } = canvas
|
|
311
|
+
const { width: canvasWidth, height: canvasHeight } = canvas;
|
|
265
312
|
const ctx = canvas.getContext('2d');
|
|
266
313
|
if (ctx) {
|
|
267
314
|
ctx.restore();
|
|
@@ -302,11 +349,7 @@ function drawLivenessOvalInCanvas({ canvas, oval, scaleFactor, videoEl, isStartS
|
|
|
302
349
|
}
|
|
303
350
|
function drawStaticOval(canvasEl, videoEl, videoMediaStream) {
|
|
304
351
|
const { width, height } = videoMediaStream.getTracks()[0].getSettings();
|
|
305
|
-
|
|
306
|
-
// and set canvas width/height.
|
|
307
|
-
const { width: videoScaledWidth, height: videoScaledHeight } = videoEl.getBoundingClientRect();
|
|
308
|
-
canvasEl.width = Math.ceil(videoScaledWidth);
|
|
309
|
-
canvasEl.height = Math.ceil(videoScaledHeight);
|
|
352
|
+
resizeCanvasToDisplaySize(canvasEl);
|
|
310
353
|
const ovalDetails = getStaticLivenessOvalDetails({
|
|
311
354
|
width: width,
|
|
312
355
|
height: height,
|
|
@@ -315,7 +358,7 @@ function drawStaticOval(canvasEl, videoEl, videoMediaStream) {
|
|
|
315
358
|
ovalDetails.flippedCenterX = width - ovalDetails.centerX;
|
|
316
359
|
// Compute scaleFactor which is how much our video element is scaled
|
|
317
360
|
// vs the intrinsic video resolution
|
|
318
|
-
const scaleFactor =
|
|
361
|
+
const scaleFactor = getVideoScaleFactor(videoEl);
|
|
319
362
|
// Draw oval in canvas using ovalDetails and scaleFactor
|
|
320
363
|
drawLivenessOvalInCanvas({
|
|
321
364
|
canvas: canvasEl,
|
|
@@ -427,17 +470,47 @@ function fillFractionalContext({ ctx, prevColor, nextColor, fraction, }) {
|
|
|
427
470
|
}
|
|
428
471
|
}
|
|
429
472
|
function fillOverlayCanvasFractional({ overlayCanvas, prevColor, nextColor, videoEl, ovalDetails, heightFraction, scaleFactor, }) {
|
|
430
|
-
|
|
473
|
+
// The overlay is `position: fixed`, but a transformed ancestor (common for
|
|
474
|
+
// modal dialogs) makes it relative to that ancestor instead of the viewport,
|
|
475
|
+
// so measure the canvas itself rather than assuming the window.
|
|
476
|
+
// getBoundingClientRect is in transformed (screen) space while clientWidth
|
|
477
|
+
// and scaleFactor are in layout space, so convert everything into the
|
|
478
|
+
// canvas's own layout space to also handle scale transforms.
|
|
479
|
+
const videoRect = videoEl.getBoundingClientRect();
|
|
480
|
+
const canvasRect = overlayCanvas.getBoundingClientRect();
|
|
481
|
+
const videoScreenScale = videoEl.clientWidth
|
|
482
|
+
? videoRect.width / videoEl.clientWidth
|
|
483
|
+
: 1;
|
|
484
|
+
const canvasScreenScale = overlayCanvas.clientWidth && canvasRect.width
|
|
485
|
+
? canvasRect.width / overlayCanvas.clientWidth
|
|
486
|
+
: 1;
|
|
487
|
+
// canvas pixels per camera frame pixel
|
|
488
|
+
const frameScale = (scaleFactor * videoScreenScale) / canvasScreenScale;
|
|
489
|
+
// offset of the (possibly letterboxed or cropped) video frame relative to
|
|
490
|
+
// the canvas, in canvas pixels
|
|
491
|
+
const frameX = (videoRect.x +
|
|
492
|
+
(videoRect.width - videoEl.videoWidth * scaleFactor * videoScreenScale) /
|
|
493
|
+
2 -
|
|
494
|
+
canvasRect.x) /
|
|
495
|
+
canvasScreenScale;
|
|
496
|
+
const frameY = (videoRect.y +
|
|
497
|
+
(videoRect.height -
|
|
498
|
+
videoEl.videoHeight * scaleFactor * videoScreenScale) /
|
|
499
|
+
2 -
|
|
500
|
+
canvasRect.y) /
|
|
501
|
+
canvasScreenScale;
|
|
431
502
|
const { flippedCenterX, centerY, width, height } = ovalDetails;
|
|
432
|
-
const updatedCenterX = flippedCenterX *
|
|
433
|
-
const updatedCenterY = centerY *
|
|
434
|
-
const canvasWidth = overlayCanvas.width;
|
|
435
|
-
const canvasHeight = overlayCanvas.height;
|
|
503
|
+
const updatedCenterX = flippedCenterX * frameScale + frameX;
|
|
504
|
+
const updatedCenterY = centerY * frameScale + frameY;
|
|
436
505
|
const ctx = overlayCanvas.getContext('2d');
|
|
506
|
+
// Because the canvas is set to 100% we need to manually set the height for the canvas to use pixel values
|
|
507
|
+
const canvasWidth = Math.ceil(overlayCanvas.clientWidth || canvasRect.width) ||
|
|
508
|
+
window.innerWidth;
|
|
509
|
+
const canvasHeight = Math.ceil(overlayCanvas.clientHeight || canvasRect.height) ||
|
|
510
|
+
window.innerHeight;
|
|
437
511
|
if (ctx) {
|
|
438
|
-
|
|
439
|
-
ctx.canvas.
|
|
440
|
-
ctx.canvas.height = window.innerHeight;
|
|
512
|
+
ctx.canvas.width = canvasWidth;
|
|
513
|
+
ctx.canvas.height = canvasHeight;
|
|
441
514
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
442
515
|
// fill the complete canvas
|
|
443
516
|
fillFractionalContext({
|
|
@@ -462,7 +535,7 @@ function fillOverlayCanvasFractional({ overlayCanvas, prevColor, nextColor, vide
|
|
|
462
535
|
});
|
|
463
536
|
// draw the oval path and fill it
|
|
464
537
|
ctx.beginPath();
|
|
465
|
-
ctx.ellipse(updatedCenterX, updatedCenterY, (width *
|
|
538
|
+
ctx.ellipse(updatedCenterX, updatedCenterY, (width * frameScale) / 2, (height * frameScale) / 2, 0, 0, 2 * Math.PI);
|
|
466
539
|
// add stroke to the oval path
|
|
467
540
|
ctx.strokeStyle = 'white';
|
|
468
541
|
ctx.lineWidth = 8;
|
|
@@ -1251,7 +1324,7 @@ function createRequestStreamGenerator(stream) {
|
|
|
1251
1324
|
};
|
|
1252
1325
|
}
|
|
1253
1326
|
|
|
1254
|
-
const VERSION = '3.
|
|
1327
|
+
const VERSION = '3.7.0';
|
|
1255
1328
|
|
|
1256
1329
|
const BASE_USER_AGENT = `ui-react-liveness/${VERSION}`;
|
|
1257
1330
|
const getLivenessUserAgent = () => {
|
|
@@ -1812,6 +1885,33 @@ _StreamRecorder_chunks = new WeakMap(), _StreamRecorder_recorder = new WeakMap()
|
|
|
1812
1885
|
tslib.__classPrivateFieldSet(this, _StreamRecorder_eventListeners, {}, "f");
|
|
1813
1886
|
};
|
|
1814
1887
|
|
|
1888
|
+
/**
|
|
1889
|
+
* Stops recording and sends `closeCode` to the streaming service so the
|
|
1890
|
+
* session is closed with a reason instead of being left to time out.
|
|
1891
|
+
*
|
|
1892
|
+
* Only call this once recording has started: `stopRecording()` resolves when
|
|
1893
|
+
* the recorder stops, so if it never started the close code is not sent.
|
|
1894
|
+
*/
|
|
1895
|
+
function closeLivenessStream(livenessStreamProvider, closeCode) {
|
|
1896
|
+
livenessStreamProvider
|
|
1897
|
+
?.stopRecording()
|
|
1898
|
+
.catch((error) => {
|
|
1899
|
+
// eslint-disable-next-line no-console
|
|
1900
|
+
console.error('Error stopping liveness recording:', error);
|
|
1901
|
+
})
|
|
1902
|
+
// send the close code even if stopping failed so the session still ends
|
|
1903
|
+
.then(() => {
|
|
1904
|
+
livenessStreamProvider.dispatchStreamEvent({
|
|
1905
|
+
type: 'closeCode',
|
|
1906
|
+
data: { closeCode },
|
|
1907
|
+
});
|
|
1908
|
+
})
|
|
1909
|
+
.catch((error) => {
|
|
1910
|
+
// eslint-disable-next-line no-console
|
|
1911
|
+
console.error('Error sending liveness close code:', error);
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1815
1915
|
const isServerSessionInformationEvent = (value) => {
|
|
1816
1916
|
return !!value
|
|
1817
1917
|
?.ServerSessionInformationEvent;
|
|
@@ -2121,6 +2221,12 @@ const livenessMachine = xstate.createMachine({
|
|
|
2121
2221
|
'sendTimeoutAfterOvalDrawingDelay',
|
|
2122
2222
|
],
|
|
2123
2223
|
initial: 'ovalDrawing',
|
|
2224
|
+
on: {
|
|
2225
|
+
VIDEO_RESIZED: {
|
|
2226
|
+
internal: true,
|
|
2227
|
+
actions: ['updateOvalScaleFactor', 'redrawLivenessOval'],
|
|
2228
|
+
},
|
|
2229
|
+
},
|
|
2124
2230
|
states: {
|
|
2125
2231
|
ovalDrawing: {
|
|
2126
2232
|
invoke: {
|
|
@@ -2393,6 +2499,34 @@ const livenessMachine = xstate.createMachine({
|
|
|
2393
2499
|
isFaceFarEnoughBeforeRecording: (_, event) => !!event.data.isFaceFarEnoughBeforeRecording,
|
|
2394
2500
|
errorState: (_, event) => event.data?.error,
|
|
2395
2501
|
}),
|
|
2502
|
+
// The oval is drawn once per check; keep it aligned when the host
|
|
2503
|
+
// container resizes after it has been drawn (e.g. modal open animation).
|
|
2504
|
+
updateOvalScaleFactor: xstate.assign({
|
|
2505
|
+
ovalAssociatedParams: (context) => {
|
|
2506
|
+
const { videoEl } = context.videoAssociatedParams ?? {};
|
|
2507
|
+
if (!context.ovalAssociatedParams?.ovalDetails || !videoEl) {
|
|
2508
|
+
return context.ovalAssociatedParams;
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
...context.ovalAssociatedParams,
|
|
2512
|
+
scaleFactor: getVideoScaleFactor(videoEl),
|
|
2513
|
+
};
|
|
2514
|
+
},
|
|
2515
|
+
}),
|
|
2516
|
+
redrawLivenessOval: (context) => {
|
|
2517
|
+
const { videoEl, canvasEl } = context.videoAssociatedParams ?? {};
|
|
2518
|
+
const { ovalDetails, scaleFactor } = context.ovalAssociatedParams ?? {};
|
|
2519
|
+
if (!videoEl || !canvasEl || !ovalDetails || !scaleFactor) {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
resizeCanvasToDisplaySize(canvasEl);
|
|
2523
|
+
drawLivenessOvalInCanvas({
|
|
2524
|
+
canvas: canvasEl,
|
|
2525
|
+
oval: ovalDetails,
|
|
2526
|
+
scaleFactor,
|
|
2527
|
+
videoEl,
|
|
2528
|
+
});
|
|
2529
|
+
},
|
|
2396
2530
|
updateOvalAndFaceDetailsPostDraw: xstate.assign({
|
|
2397
2531
|
ovalAssociatedParams: (context, event) => ({
|
|
2398
2532
|
...context.ovalAssociatedParams,
|
|
@@ -2572,12 +2706,7 @@ const livenessMachine = xstate.createMachine({
|
|
|
2572
2706
|
else if (context.errorState === undefined) {
|
|
2573
2707
|
closeCode = WS_CLOSURE_CODE.USER_CANCEL;
|
|
2574
2708
|
}
|
|
2575
|
-
context.livenessStreamProvider
|
|
2576
|
-
context.livenessStreamProvider?.dispatchStreamEvent({
|
|
2577
|
-
type: 'closeCode',
|
|
2578
|
-
data: { closeCode },
|
|
2579
|
-
});
|
|
2580
|
-
});
|
|
2709
|
+
closeLivenessStream(context.livenessStreamProvider, closeCode);
|
|
2581
2710
|
},
|
|
2582
2711
|
freezeStream: (context) => {
|
|
2583
2712
|
const { videoMediaStream, videoEl } = context.videoAssociatedParams;
|
|
@@ -2809,7 +2938,7 @@ const livenessMachine = xstate.createMachine({
|
|
|
2809
2938
|
},
|
|
2810
2939
|
async detectInitialFaceAndDrawOval(context) {
|
|
2811
2940
|
const { parsedSessionInformation } = context;
|
|
2812
|
-
const { videoEl, canvasEl
|
|
2941
|
+
const { videoEl, canvasEl } = context.videoAssociatedParams;
|
|
2813
2942
|
const { faceDetector } = context.ovalAssociatedParams;
|
|
2814
2943
|
// initialize models
|
|
2815
2944
|
await faceDetector.modelLoadingPromise;
|
|
@@ -2840,20 +2969,12 @@ const livenessMachine = xstate.createMachine({
|
|
|
2840
2969
|
if (!initialFace) {
|
|
2841
2970
|
return { faceMatchState, illuminationState };
|
|
2842
2971
|
}
|
|
2843
|
-
//
|
|
2844
|
-
//
|
|
2845
|
-
|
|
2846
|
-
if (isMobile) {
|
|
2847
|
-
canvasEl.width = window.innerWidth;
|
|
2848
|
-
canvasEl.height = window.innerHeight;
|
|
2849
|
-
}
|
|
2850
|
-
else {
|
|
2851
|
-
canvasEl.width = videoScaledWidth;
|
|
2852
|
-
canvasEl.height = videoScaledHeight;
|
|
2853
|
-
}
|
|
2972
|
+
// Size the canvas to its container: the full screen on mobile and the
|
|
2973
|
+
// video element otherwise.
|
|
2974
|
+
resizeCanvasToDisplaySize(canvasEl);
|
|
2854
2975
|
// Compute scaleFactor which is how much our video element is scaled
|
|
2855
2976
|
// vs the intrinsic video resolution
|
|
2856
|
-
const scaleFactor =
|
|
2977
|
+
const scaleFactor = getVideoScaleFactor(videoEl);
|
|
2857
2978
|
// generate oval details from initialFace and video dimensions
|
|
2858
2979
|
const ovalDetails = getOvalDetailsFromSessionInformation({
|
|
2859
2980
|
parsedSessionInformation: parsedSessionInformation,
|
|
@@ -2938,7 +3059,7 @@ const livenessMachine = xstate.createMachine({
|
|
|
2938
3059
|
if (freshnessColorsComplete) {
|
|
2939
3060
|
return;
|
|
2940
3061
|
}
|
|
2941
|
-
const { ovalDetails
|
|
3062
|
+
const { ovalDetails } = ovalAssociatedParams;
|
|
2942
3063
|
const { videoEl } = videoAssociatedParams;
|
|
2943
3064
|
const completed = await colorSequenceDisplay.startSequences({
|
|
2944
3065
|
onSequenceColorChange: ({ sequenceColor, prevSequenceColor, heightFraction, }) => {
|
|
@@ -2948,7 +3069,8 @@ const livenessMachine = xstate.createMachine({
|
|
|
2948
3069
|
ovalDetails: ovalDetails,
|
|
2949
3070
|
nextColor: sequenceColor,
|
|
2950
3071
|
prevColor: prevSequenceColor,
|
|
2951
|
-
|
|
3072
|
+
// read per frame so a resize during the sequence stays aligned
|
|
3073
|
+
scaleFactor: getVideoScaleFactor(videoEl),
|
|
2952
3074
|
videoEl: videoEl,
|
|
2953
3075
|
});
|
|
2954
3076
|
},
|
|
@@ -3123,7 +3245,7 @@ var LivenessClassNames;
|
|
|
3123
3245
|
LivenessClassNames["VideoAnchor"] = "amplify-liveness-video-anchor";
|
|
3124
3246
|
})(LivenessClassNames || (LivenessClassNames = {}));
|
|
3125
3247
|
|
|
3126
|
-
const CancelButton = ({ ariaLabel }) => {
|
|
3248
|
+
const CancelButton = ({ ariaLabel, Component, }) => {
|
|
3127
3249
|
const [state, send] = useLivenessActor();
|
|
3128
3250
|
const isFinalState = state.done;
|
|
3129
3251
|
const handleClick = () => {
|
|
@@ -3133,6 +3255,9 @@ const CancelButton = ({ ariaLabel }) => {
|
|
|
3133
3255
|
};
|
|
3134
3256
|
if (isFinalState)
|
|
3135
3257
|
return null;
|
|
3258
|
+
if (Component) {
|
|
3259
|
+
return (React__namespace["default"].createElement(Component, { onCancel: handleClick, cancelLivenessCheckText: ariaLabel }));
|
|
3260
|
+
}
|
|
3136
3261
|
return (React__namespace["default"].createElement(uiReact.Button, { autoFocus: true, variation: "link", onClick: handleClick, size: "large", className: LivenessClassNames.CancelButton, "aria-label": ariaLabel },
|
|
3137
3262
|
React__namespace["default"].createElement(internal.IconClose, { "aria-hidden": "true", "data-testid": "close-icon" })));
|
|
3138
3263
|
};
|
|
@@ -3492,9 +3617,9 @@ const DefaultRecordingIcon = ({ recordingIndicatorText, }) => {
|
|
|
3492
3617
|
return (React__namespace["default"].createElement(uiReact.View, { className: LivenessClassNames.RecordingIconContainer },
|
|
3493
3618
|
React__namespace["default"].createElement(RecordingIcon, null, recordingIndicatorText)));
|
|
3494
3619
|
};
|
|
3495
|
-
const DefaultCancelButton = ({ cancelLivenessCheckText, }) => {
|
|
3620
|
+
const DefaultCancelButton = ({ cancelLivenessCheckText, CancelButton: CancelButton$1, }) => {
|
|
3496
3621
|
return (React__namespace["default"].createElement(uiReact.View, { className: LivenessClassNames.CancelContainer },
|
|
3497
|
-
React__namespace["default"].createElement(CancelButton, { ariaLabel: cancelLivenessCheckText })));
|
|
3622
|
+
React__namespace["default"].createElement(CancelButton, { ariaLabel: cancelLivenessCheckText, Component: CancelButton$1 })));
|
|
3498
3623
|
};
|
|
3499
3624
|
|
|
3500
3625
|
const CameraSelector = (props) => {
|
|
@@ -3511,7 +3636,11 @@ const selectVideoStream = createLivenessSelector((state) => state.context.videoA
|
|
|
3511
3636
|
const selectFaceMatchPercentage = createLivenessSelector((state) => state.context.faceMatchAssociatedParams?.faceMatchPercentage);
|
|
3512
3637
|
const selectFaceMatchState = createLivenessSelector((state) => state.context.faceMatchAssociatedParams?.faceMatchState);
|
|
3513
3638
|
const selectSelectedDeviceId = createLivenessSelector((state) => state.context.videoAssociatedParams?.selectedDeviceId);
|
|
3639
|
+
const selectDisableStartScreen = createLivenessSelector((state) => state.context.componentProps?.disableStartScreen);
|
|
3514
3640
|
const selectSelectableDevices = createLivenessSelector((state) => state.context.videoAssociatedParams?.selectableDevices);
|
|
3641
|
+
// how long the video size must be stable during recording before the session
|
|
3642
|
+
// oval is redrawn
|
|
3643
|
+
const VIDEO_RESIZED_DEBOUNCE_MS = 100;
|
|
3515
3644
|
const showMatchIndicatorStates = [
|
|
3516
3645
|
FaceMatchState.TOO_FAR,
|
|
3517
3646
|
FaceMatchState.CANT_IDENTIFY,
|
|
@@ -3527,7 +3656,7 @@ const MemoizedMatchIndicator = React__namespace["default"].memo(MatchIndicator);
|
|
|
3527
3656
|
const LivenessCameraModule = (props) => {
|
|
3528
3657
|
const { isMobileScreen, isRecordingStopped, instructionDisplayText, streamDisplayText, hintDisplayText, errorDisplayText, cameraDisplayText, components: customComponents, testId, } = props;
|
|
3529
3658
|
const { cancelLivenessCheckText, recordingIndicatorText } = streamDisplayText;
|
|
3530
|
-
const { ErrorView = FaceLivenessErrorModal, PhotosensitiveWarning = DefaultPhotosensitiveWarning, } = customComponents ?? {};
|
|
3659
|
+
const { ErrorView = FaceLivenessErrorModal, PhotosensitiveWarning = DefaultPhotosensitiveWarning, CancelButton, } = customComponents ?? {};
|
|
3531
3660
|
const [state, send] = useLivenessActor();
|
|
3532
3661
|
const isFaceMovementChallenge = useLivenessSelector(selectChallengeType) === FACE_MOVEMENT_CHALLENGE.type;
|
|
3533
3662
|
const videoStream = useLivenessSelector(selectVideoStream);
|
|
@@ -3537,9 +3666,11 @@ const LivenessCameraModule = (props) => {
|
|
|
3537
3666
|
const faceMatchPercentage = useLivenessSelector(selectFaceMatchPercentage);
|
|
3538
3667
|
const faceMatchState = useLivenessSelector(selectFaceMatchState);
|
|
3539
3668
|
const errorState = useLivenessSelector(selectErrorState);
|
|
3669
|
+
const disableStartScreen = useLivenessSelector(selectDisableStartScreen);
|
|
3540
3670
|
const colorMode = internal.useColorMode();
|
|
3541
3671
|
const { videoRef, videoWidth, videoHeight } = useMediaStreamInVideo(videoStream);
|
|
3542
3672
|
const canvasRef = React.useRef(null);
|
|
3673
|
+
const videoAnchorRef = React.useRef(null);
|
|
3543
3674
|
const freshnessColorRef = React.useRef(null);
|
|
3544
3675
|
const [isCameraReady, setIsCameraReady] = React.useState(false);
|
|
3545
3676
|
const [isMetadataLoaded, setIsMetadataLoaded] = React.useState(false);
|
|
@@ -3598,6 +3729,84 @@ const LivenessCameraModule = (props) => {
|
|
|
3598
3729
|
lightModePreference.addEventListener('change', updateColorModeHandler);
|
|
3599
3730
|
};
|
|
3600
3731
|
}, [videoRef, videoStream, colorMode, isStartView, isMetadataLoaded]);
|
|
3732
|
+
// Size the video to fill the anchor, which grows to fill the host container
|
|
3733
|
+
// when the host gives it a height (e.g. a modal body)
|
|
3734
|
+
const layoutVideo = React__namespace["default"].useCallback(() => {
|
|
3735
|
+
const anchor = videoAnchorRef.current;
|
|
3736
|
+
const video = videoRef.current;
|
|
3737
|
+
if (!anchor || !video?.videoWidth || !video.videoHeight) {
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
const { width, height, left, top } = getVideoFillLayout({
|
|
3741
|
+
containerWidth: anchor.clientWidth,
|
|
3742
|
+
containerHeight: anchor.clientHeight,
|
|
3743
|
+
videoWidth: video.videoWidth,
|
|
3744
|
+
videoHeight: video.videoHeight,
|
|
3745
|
+
});
|
|
3746
|
+
video.style.width = `${width}px`;
|
|
3747
|
+
video.style.height = `${height}px`;
|
|
3748
|
+
video.style.left = `${left}px`;
|
|
3749
|
+
video.style.top = `${top}px`;
|
|
3750
|
+
}, [videoRef]);
|
|
3751
|
+
// Keep the oval aligned with the video when the host container resizes,
|
|
3752
|
+
// e.g. a modal open animation or a responsive layout change. Kept in a ref
|
|
3753
|
+
// so the observer below always calls the latest version.
|
|
3754
|
+
const redrawOnResizeRef = React__namespace["default"].useRef();
|
|
3755
|
+
const recordingResizeTimeoutRef = React__namespace["default"].useRef(null);
|
|
3756
|
+
React__namespace["default"].useLayoutEffect(() => {
|
|
3757
|
+
redrawOnResizeRef.current = () => {
|
|
3758
|
+
layoutVideo();
|
|
3759
|
+
if (isStartView && isMetadataLoaded && canvasRef.current && videoStream) {
|
|
3760
|
+
drawStaticOval(canvasRef.current, videoRef.current, videoStream);
|
|
3761
|
+
}
|
|
3762
|
+
else if (isRecording) {
|
|
3763
|
+
// the video is laid out every frame above; only redraw the session
|
|
3764
|
+
// oval once the size settles to avoid a machine transition per frame
|
|
3765
|
+
if (recordingResizeTimeoutRef.current) {
|
|
3766
|
+
clearTimeout(recordingResizeTimeoutRef.current);
|
|
3767
|
+
}
|
|
3768
|
+
recordingResizeTimeoutRef.current = setTimeout(() => {
|
|
3769
|
+
recordingResizeTimeoutRef.current = null;
|
|
3770
|
+
send({ type: 'VIDEO_RESIZED' });
|
|
3771
|
+
}, VIDEO_RESIZED_DEBOUNCE_MS);
|
|
3772
|
+
}
|
|
3773
|
+
};
|
|
3774
|
+
});
|
|
3775
|
+
React__namespace["default"].useEffect(() => {
|
|
3776
|
+
const anchor = videoAnchorRef.current;
|
|
3777
|
+
if (!anchor || typeof ResizeObserver === 'undefined') {
|
|
3778
|
+
return;
|
|
3779
|
+
}
|
|
3780
|
+
let lastWidth;
|
|
3781
|
+
let lastHeight;
|
|
3782
|
+
let frameId;
|
|
3783
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
3784
|
+
const { clientWidth, clientHeight } = anchor;
|
|
3785
|
+
if (clientWidth === lastWidth && clientHeight === lastHeight) {
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
lastWidth = clientWidth;
|
|
3789
|
+
lastHeight = clientHeight;
|
|
3790
|
+
// coalesce into a single redraw per frame
|
|
3791
|
+
frameId ?? (frameId = requestAnimationFrame(() => {
|
|
3792
|
+
frameId = undefined;
|
|
3793
|
+
redrawOnResizeRef.current?.();
|
|
3794
|
+
}));
|
|
3795
|
+
});
|
|
3796
|
+
resizeObserver.observe(anchor);
|
|
3797
|
+
return () => {
|
|
3798
|
+
resizeObserver.disconnect();
|
|
3799
|
+
if (frameId !== undefined) {
|
|
3800
|
+
cancelAnimationFrame(frameId);
|
|
3801
|
+
}
|
|
3802
|
+
if (recordingResizeTimeoutRef.current) {
|
|
3803
|
+
clearTimeout(recordingResizeTimeoutRef.current);
|
|
3804
|
+
}
|
|
3805
|
+
};
|
|
3806
|
+
// The camera permission check returns early (below) without rendering the
|
|
3807
|
+
// video anchor, so the ref is only set once `isCheckingCamera` is false;
|
|
3808
|
+
// re-run then to start observing the newly mounted anchor.
|
|
3809
|
+
}, [isCheckingCamera]);
|
|
3601
3810
|
React__namespace["default"].useLayoutEffect(() => {
|
|
3602
3811
|
if (isCameraReady) {
|
|
3603
3812
|
send({
|
|
@@ -3629,6 +3838,7 @@ const LivenessCameraModule = (props) => {
|
|
|
3629
3838
|
setIsCameraReady(true);
|
|
3630
3839
|
};
|
|
3631
3840
|
const handleLoadedMetadata = () => {
|
|
3841
|
+
layoutVideo();
|
|
3632
3842
|
setIsMetadataLoaded(true);
|
|
3633
3843
|
};
|
|
3634
3844
|
const beginLivenessCheck = React__namespace["default"].useCallback(() => {
|
|
@@ -3663,7 +3873,9 @@ const LivenessCameraModule = (props) => {
|
|
|
3663
3873
|
// We don't show full screen camera on the pre check screen (isStartView/isWaitingForCamera)
|
|
3664
3874
|
const shouldShowFullScreenCamera = isMobileScreen && !isStartView && !shouldShowCenteredLoader;
|
|
3665
3875
|
return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null,
|
|
3666
|
-
!isFaceMovementChallenge &&
|
|
3876
|
+
!isFaceMovementChallenge &&
|
|
3877
|
+
!disableStartScreen &&
|
|
3878
|
+
photoSensitivityWarning,
|
|
3667
3879
|
shouldShowCenteredLoader && (React__namespace["default"].createElement(uiReact.Flex, { className: LivenessClassNames.ConnectingLoader },
|
|
3668
3880
|
React__namespace["default"].createElement(uiReact.Loader, { size: "large", className: LivenessClassNames.Loader, "data-testid": "centered-loader" }),
|
|
3669
3881
|
React__namespace["default"].createElement(uiReact.Text, { className: LivenessClassNames.LandscapeErrorModalHeader }, hintDisplayText.hintConnectingText))),
|
|
@@ -3671,7 +3883,10 @@ const LivenessCameraModule = (props) => {
|
|
|
3671
3883
|
`${LivenessClassNames.CameraModule}--mobile`), "data-testid": testId, gap: "zero" },
|
|
3672
3884
|
React__namespace["default"].createElement(Overlay, { horizontal: "center", vertical: isRecording && !isFlashingFreshness ? 'start' : 'space-between', className: LivenessClassNames.InstructionOverlay },
|
|
3673
3885
|
isRecording && (React__namespace["default"].createElement(DefaultRecordingIcon, { recordingIndicatorText: recordingIndicatorText })),
|
|
3674
|
-
!isStartView &&
|
|
3886
|
+
!isStartView &&
|
|
3887
|
+
!isWaitingForCamera &&
|
|
3888
|
+
!isCheckSucceeded &&
|
|
3889
|
+
CancelButton !== null && (React__namespace["default"].createElement(DefaultCancelButton, { cancelLivenessCheckText: cancelLivenessCheckText, CancelButton: CancelButton })),
|
|
3675
3890
|
React__namespace["default"].createElement(uiReact.Flex, { className: ui.classNames(LivenessClassNames.Hint, shouldShowFullScreenCamera && `${LivenessClassNames.Hint}--mobile`) },
|
|
3676
3891
|
React__namespace["default"].createElement(Hint, { hintDisplayText: hintDisplayText })),
|
|
3677
3892
|
errorState && (React__namespace["default"].createElement(ErrorView, { onRetry: () => {
|
|
@@ -3684,7 +3899,7 @@ const LivenessCameraModule = (props) => {
|
|
|
3684
3899
|
!isFlashingFreshness &&
|
|
3685
3900
|
showMatchIndicatorStates.includes(faceMatchState) ? (React__namespace["default"].createElement(MemoizedMatchIndicator, { percentage: Math.ceil(faceMatchPercentage) })) : null),
|
|
3686
3901
|
React__namespace["default"].createElement(uiReact.View, { as: "canvas", ref: freshnessColorRef, className: LivenessClassNames.FreshnessCanvas, hidden: true }),
|
|
3687
|
-
React__namespace["default"].createElement(uiReact.View, { className: LivenessClassNames.VideoAnchor, style: {
|
|
3902
|
+
React__namespace["default"].createElement(uiReact.View, { ref: videoAnchorRef, className: LivenessClassNames.VideoAnchor, style: {
|
|
3688
3903
|
aspectRatio: `${aspectRatio}`,
|
|
3689
3904
|
} },
|
|
3690
3905
|
React__namespace["default"].createElement("video", { ref: videoRef, muted: true, autoPlay: true, playsInline: true, width: mediaWidth, height: mediaHeight, onCanPlay: handleMediaPlay, onLoadedMetadata: handleLoadedMetadata, "data-testid": "video", className: ui.classNames(LivenessClassNames.Video, isCameraUserFacing && LivenessClassNames.UserFacingVideo, isRecordingStopped && LivenessClassNames.FadeOut), "aria-label": cameraDisplayText.a11yVideoLabelText }),
|
|
@@ -3778,8 +3993,8 @@ const LivenessCheck = ({ instructionDisplayText, hintDisplayText, cameraDisplayT
|
|
|
3778
3993
|
? cameraMinSpecificationsMessageText
|
|
3779
3994
|
: cameraNotFoundMessageText),
|
|
3780
3995
|
React__namespace.createElement(uiReact.Button, { variation: "primary", type: "button", onClick: recheckCameraPermissions }, retryCameraPermissionsText),
|
|
3781
|
-
React__namespace.createElement(uiReact.View, { position: "absolute", top: "medium", right: "medium" },
|
|
3782
|
-
React__namespace.createElement(CancelButton, { ariaLabel: cancelLivenessCheckText }))));
|
|
3996
|
+
components?.CancelButton !== null && (React__namespace.createElement(uiReact.View, { position: "absolute", top: "medium", right: "medium" },
|
|
3997
|
+
React__namespace.createElement(CancelButton, { ariaLabel: cancelLivenessCheckText, Component: components?.CancelButton })))));
|
|
3783
3998
|
}
|
|
3784
3999
|
else {
|
|
3785
4000
|
return (React__namespace.createElement(LivenessCameraModule, { isMobileScreen: isMobile, isRecordingStopped: isRecordingStopped, instructionDisplayText: instructionDisplayText, streamDisplayText: streamDisplayText, hintDisplayText: hintDisplayText, errorDisplayText: errorDisplayText, cameraDisplayText: cameraDisplayText, components: components }));
|
|
@@ -3914,6 +4129,22 @@ function FaceLivenessDetectorCore(props) {
|
|
|
3914
4129
|
},
|
|
3915
4130
|
},
|
|
3916
4131
|
});
|
|
4132
|
+
React__namespace.useEffect(() => {
|
|
4133
|
+
// Track the stream provider as the state changes: by the time this
|
|
4134
|
+
// effect is cleaned up, the service has been stopped and reset.
|
|
4135
|
+
let livenessStreamProvider;
|
|
4136
|
+
const { unsubscribe } = service.subscribe(({ context }) => {
|
|
4137
|
+
({ livenessStreamProvider } = context);
|
|
4138
|
+
});
|
|
4139
|
+
return () => {
|
|
4140
|
+
unsubscribe();
|
|
4141
|
+
// Unmounting mid-check (e.g. the host modal is closed) bypasses the
|
|
4142
|
+
// CANCEL event, so close the stream the same way the cancel button does
|
|
4143
|
+
if (livenessStreamProvider?.isRecording()) {
|
|
4144
|
+
closeLivenessStream(livenessStreamProvider, WS_CLOSURE_CODE.USER_CANCEL);
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
4147
|
+
}, [service]);
|
|
3917
4148
|
return (React__namespace.createElement(uiReact.View, { className: DETECTOR_CLASS_NAME, testId: DETECTOR_CLASS_NAME },
|
|
3918
4149
|
React__namespace.createElement(FaceLivenessDetectorProvider, { componentProps: props, service: service },
|
|
3919
4150
|
React__namespace.createElement(uiReact.Flex, { direction: "column", ref: currElementRef },
|
package/dist/styles.css
CHANGED
|
@@ -4246,7 +4246,21 @@ html[dir=rtl] .amplify-field-group__inner-start {
|
|
|
4246
4246
|
opacity: 0;
|
|
4247
4247
|
}
|
|
4248
4248
|
}
|
|
4249
|
+
.liveness-detector {
|
|
4250
|
+
display: flex;
|
|
4251
|
+
flex-direction: column;
|
|
4252
|
+
height: 100%;
|
|
4253
|
+
min-height: 0;
|
|
4254
|
+
}
|
|
4255
|
+
.liveness-detector > .amplify-flex,
|
|
4256
|
+
.liveness-detector .liveness-detector-check {
|
|
4257
|
+
flex: 1 1 auto;
|
|
4258
|
+
min-height: 0;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4249
4261
|
.amplify-liveness-camera-module {
|
|
4262
|
+
flex: 1 1 auto;
|
|
4263
|
+
min-height: 0;
|
|
4250
4264
|
flex-direction: column;
|
|
4251
4265
|
align-items: center;
|
|
4252
4266
|
justify-content: center;
|
|
@@ -4323,6 +4337,13 @@ html[dir=rtl] .amplify-field-group__inner-start {
|
|
|
4323
4337
|
.amplify-liveness-video-anchor {
|
|
4324
4338
|
position: relative;
|
|
4325
4339
|
width: 100%;
|
|
4340
|
+
flex: 1 1 auto;
|
|
4341
|
+
min-height: 200px;
|
|
4342
|
+
overflow: hidden;
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
.amplify-liveness-camera-module--mobile .amplify-liveness-video-anchor {
|
|
4346
|
+
flex: none;
|
|
4326
4347
|
}
|
|
4327
4348
|
|
|
4328
4349
|
.amplify-liveness-recording-icon-container {
|
|
@@ -4663,6 +4684,7 @@ html[dir=rtl] .amplify-field-group__inner-start {
|
|
|
4663
4684
|
justify-content: center;
|
|
4664
4685
|
text-align: center;
|
|
4665
4686
|
height: 480px;
|
|
4687
|
+
max-height: 100%;
|
|
4666
4688
|
}
|
|
4667
4689
|
|
|
4668
4690
|
.amplify-menu__wrapper {
|
|
@@ -8,6 +8,7 @@ export declare const selectVideoStream: import("../hooks").LivenessSelectorFn<Me
|
|
|
8
8
|
export declare const selectFaceMatchPercentage: import("../hooks").LivenessSelectorFn<number | undefined>;
|
|
9
9
|
export declare const selectFaceMatchState: import("../hooks").LivenessSelectorFn<FaceMatchState | undefined>;
|
|
10
10
|
export declare const selectSelectedDeviceId: import("../hooks").LivenessSelectorFn<string | undefined>;
|
|
11
|
+
export declare const selectDisableStartScreen: import("../hooks").LivenessSelectorFn<boolean | undefined>;
|
|
11
12
|
export declare const selectSelectableDevices: import("../hooks").LivenessSelectorFn<MediaDeviceInfo[] | undefined>;
|
|
12
13
|
export interface LivenessCameraModuleProps {
|
|
13
14
|
isMobileScreen: boolean;
|
|
@@ -66,7 +66,7 @@ export interface LivenessContext {
|
|
|
66
66
|
shouldDisconnect: boolean | undefined;
|
|
67
67
|
videoAssociatedParams: VideoAssociatedParams | undefined;
|
|
68
68
|
}
|
|
69
|
-
export type LivenessEventTypes = 'BEGIN' | 'CONNECTION_TIMEOUT' | 'START_RECORDING' | 'TIMEOUT' | 'ERROR' | 'CANCEL' | 'SET_SESSION_INFO' | 'DISCONNECT_EVENT' | 'SET_DOM_AND_CAMERA_DETAILS' | 'UPDATE_DEVICE_AND_STREAM' | 'CAMERA_NOT_FOUND' | 'SERVER_ERROR' | 'RUNTIME_ERROR' | 'RETRY_CAMERA_CHECK' | 'MOBILE_LANDSCAPE_WARNING';
|
|
69
|
+
export type LivenessEventTypes = 'BEGIN' | 'CONNECTION_TIMEOUT' | 'START_RECORDING' | 'TIMEOUT' | 'ERROR' | 'CANCEL' | 'SET_SESSION_INFO' | 'DISCONNECT_EVENT' | 'SET_DOM_AND_CAMERA_DETAILS' | 'UPDATE_DEVICE_AND_STREAM' | 'CAMERA_NOT_FOUND' | 'SERVER_ERROR' | 'RUNTIME_ERROR' | 'RETRY_CAMERA_CHECK' | 'MOBILE_LANDSCAPE_WARNING' | 'VIDEO_RESIZED';
|
|
70
70
|
export type LivenessEventData = Record<PropertyKey, any>;
|
|
71
71
|
export interface LivenessEvent {
|
|
72
72
|
type: LivenessEventTypes;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StreamRecorder } from './StreamRecorder';
|
|
2
|
+
/**
|
|
3
|
+
* Stops recording and sends `closeCode` to the streaming service so the
|
|
4
|
+
* session is closed with a reason instead of being left to time out.
|
|
5
|
+
*
|
|
6
|
+
* Only call this once recording has started: `stopRecording()` resolves when
|
|
7
|
+
* the recorder stops, so if it never started the close code is not sent.
|
|
8
|
+
*/
|
|
9
|
+
export declare function closeLivenessStream(livenessStreamProvider: StreamRecorder | undefined, closeCode: number): void;
|
|
@@ -8,3 +8,4 @@ export { createRequestStreamGenerator, createSessionStartEvent, createSessionEnd
|
|
|
8
8
|
export { createStreamingClient } from './createStreamingClient';
|
|
9
9
|
export { isFaceMovementAndLightChallenge, isFaceMovementChallenge, createSessionInfoFromServerSessionInformation, } from './sessionInformation';
|
|
10
10
|
export { StreamRecorder } from './StreamRecorder';
|
|
11
|
+
export { closeLivenessStream } from './closeLivenessStream';
|
|
@@ -36,6 +36,36 @@ export declare function getStaticLivenessOvalDetails({ width, height, widthSeed,
|
|
|
36
36
|
ratioMultiplier?: number;
|
|
37
37
|
ovalHeightWidthRatio?: number;
|
|
38
38
|
}): LivenessOvalDetails;
|
|
39
|
+
/**
|
|
40
|
+
* Returns how much the intrinsic video frame is scaled inside the rendered
|
|
41
|
+
* video element. The video element uses the default `object-fit: contain`,
|
|
42
|
+
* so when the host container gives it a box that does not match the camera
|
|
43
|
+
* aspect ratio the frame is letterboxed and the smaller ratio applies.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getVideoScaleFactor(videoEl: HTMLVideoElement): number;
|
|
46
|
+
export declare const MIN_VISIBLE_FRAME_WIDTH_FRACTION = 0.7;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the size and position of the video element so the camera frame
|
|
49
|
+
* fills as much of its container as possible without cropping the oval.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getVideoFillLayout({ containerWidth, containerHeight, videoWidth, videoHeight, }: {
|
|
52
|
+
containerWidth: number;
|
|
53
|
+
containerHeight: number;
|
|
54
|
+
videoWidth: number;
|
|
55
|
+
videoHeight: number;
|
|
56
|
+
}): {
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
left: number;
|
|
60
|
+
top: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Sizes the canvas drawing buffer to match its parent's box: the full screen
|
|
64
|
+
* on mobile and the video anchor box otherwise. The anchor can differ from the
|
|
65
|
+
* video element's box, which is centered in it and may be letterboxed or
|
|
66
|
+
* cropped on the sides.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement): void;
|
|
39
69
|
/**
|
|
40
70
|
* Draws the provided liveness oval on the canvas.
|
|
41
71
|
*/
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
export interface FaceLivenessCancelButtonProps {
|
|
3
|
+
/**
|
|
4
|
+
* Cancels the liveness check, same as the default cancel button
|
|
5
|
+
*/
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
/**
|
|
8
|
+
* Resolved `cancelLivenessCheckText` display text, used as the default
|
|
9
|
+
* button's accessible name
|
|
10
|
+
*/
|
|
11
|
+
cancelLivenessCheckText: string;
|
|
12
|
+
}
|
|
2
13
|
export interface CancelButtonProps {
|
|
3
14
|
ariaLabel: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom cancel button provided through `components.CancelButton`
|
|
17
|
+
*/
|
|
18
|
+
Component?: React.ComponentType<FaceLivenessCancelButtonProps>;
|
|
4
19
|
}
|
|
5
20
|
export declare const CancelButton: React.FC<CancelButtonProps>;
|