Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58d9ba1
feat: add ability to pass device in and out of liveness check
jasonfill Jun 14, 2025
f00e7fd
chore: clean up some formatting issues
jasonfill Jun 14, 2025
16f1c07
chore: fix additional formatting issues
jasonfill Jun 14, 2025
1216c8f
chore: fix more formatting
jasonfill Jun 14, 2025
54d64f3
chore: fix some formatting items
jasonfill Jun 14, 2025
afd93c1
chore: additional formatting changes
jasonfill Jun 14, 2025
e8a5653
chore: some resolutions were removed, replaced them
jasonfill Jun 14, 2025
b11b3ce
chore: formatting
jasonfill Jun 14, 2025
85f47e0
chore: remove duplicate items in package
jasonfill Jun 14, 2025
8ca425d
chore: formatting
jasonfill Jun 15, 2025
933013b
chore: add ability to provide default device label to select that dev…
riasatali42 Jul 16, 2025
afb5077
chore: added callback method for camera changed and if not default ca…
riasatali42 Jul 16, 2025
8c9f3e9
chore: added selectableDevices array check
riasatali42 Jul 17, 2025
a89ff4d
chore: added default device info in machine tests for unit testing
riasatali42 Jul 17, 2025
8905251
chore: default device info for unit testing default device pass in
riasatali42 Jul 21, 2025
36e0fd3
chore: unit tests added for default device not found and camera switc…
riasatali42 Jul 22, 2025
497bfb0
chore: added device selection priority unit tests
riasatali42 Jul 22, 2025
f0f7923
docs: added changeset for minor changes
riasatali42 Jul 22, 2025
3c75e1d
fix: resolved merge conflicts
riasatali42 Jul 28, 2025
0a3ad1b
fix: fixed mock device info after resolving merge conflicts
riasatali42 Jul 28, 2025
724dca3
chore: added updated unit tests for passed in device label, callCamer…
riasatali42 Jul 28, 2025
7a701c1
Merge branch 'main' into react-liveness/provide-default-device-info
riasatali42 Jul 28, 2025
d1bede1
fix: fixed ESlint issue on livenesscameramodule
riasatali42 Jul 29, 2025
af9b3bf
fix: fixed eslint issue on adding type on error and replaced operator
riasatali42 Jul 29, 2025
bfd66d5
Merge branch 'react-liveness/provide-default-device-info' of https://…
riasatali42 Jul 29, 2025
3662944
fix: resolved PR feedback, added console error, removed device info f…
riasatali42 Aug 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chilly-olives-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/ui-react-liveness': minor
---

Pass default device info using ID and label; prioritize label over ID. Emit detailed device info on camera selection/change. Add warnings for default device not found and camera change events.
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,40 @@ export const LivenessCameraModule = (
recording: 'flashFreshnessColors',
});

// Update the device if the selectedDeviceId changes
React.useEffect(() => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to create a new effect here instead of reusing the existing onCameraChange callback?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can discard that as the onCameraChange is already handling everything. So, I discarded.

if (
selectedDeviceId &&
Array.isArray(selectableDevices) &&
selectableDevices.length > 0
) {
const device = selectableDevices.find(
(d) => d.deviceId === selectedDeviceId
);
if (device) {
// Update the device in the state machine
const changeCamera = async () => {
try {
const newStream = await navigator.mediaDevices.getUserMedia({
video: {
...videoConstraints,
deviceId: { exact: selectedDeviceId },
},
audio: false,
});
send({
type: 'UPDATE_DEVICE_AND_STREAM',
data: { newDeviceId: selectedDeviceId, newStream },
});
} catch (error: any) {
throw new Error('Error updating device:');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's include the original error message. ex:

throw new Error(`Error updating device: ${error.message}`);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved.

}
};
changeCamera();
}
}
}, [selectedDeviceId, selectableDevices, send, videoConstraints]);

// Android/Firefox and iOS flip the values of width/height returned from
// getUserMedia, so we'll reset these in useLayoutEffect with the videoRef
// element's intrinsic videoWidth and videoHeight attributes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import * as React from 'react';
import { screen, waitFor } from '@testing-library/react';
import {
screen,
waitFor,
fireEvent,
act,
render,
} from '@testing-library/react';
import { when, resetAllWhenMocks } from 'jest-when';
import { LivenessClassNames } from '../../types/classNames';

import { mockVideoMediaStream } from '../../service/utils/__mocks__/testUtils';
import {
renderWithLivenessProvider,
getMockedFunction,
Expand All @@ -28,6 +34,7 @@ import { FaceMatchState } from '../../service';
import * as Device from '../../utils/device';
import { getDisplayText } from '../../utils/getDisplayText';
import { selectIsRecordingStopped } from '../LivenessCheck';
import { selectErrorState } from '../../shared';

jest.mock('../../hooks');
jest.mock('../../hooks/useLivenessSelector');
Expand All @@ -41,6 +48,15 @@ const mockUseLivenessActor = getMockedFunction(useLivenessActor);
const mockUseLivenessSelector = getMockedFunction(useLivenessSelector);
const mockUseMediaStreamInVideo = getMockedFunction(useMediaStreamInVideo);

// Mock navigator.mediaDevices.getUserMedia
const mockGetUserMedia = jest.fn();
Object.defineProperty(global.navigator, 'mediaDevices', {
value: {
getUserMedia: mockGetUserMedia,
},
writable: true,
});

const mockDevices = [
{
deviceId: '123',
Expand Down Expand Up @@ -80,6 +96,23 @@ describe('LivenessCameraModule', () => {
} = getDisplayText(undefined);
const { cancelLivenessCheckText, recordingIndicatorText } = streamDisplayText;

const mockVideoConstraints = {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user',
};

const mockSelectableDevices = [
{ deviceId: 'device-1', label: 'Camera 1' },
{ deviceId: 'device-2', label: 'Camera 2' },
{ deviceId: 'device-3', label: 'Camera 3' },
];

const mockMediaStream = {
getTracks: jest.fn(() => []),
getVideoTracks: jest.fn(() => []),
};

function mockStateMatchesAndSelectors() {
when(mockActorState.matches)
.calledWith('initCamera')
Expand All @@ -96,8 +129,18 @@ describe('LivenessCameraModule', () => {
.mockReturnValue(isNotRecording)
.calledWith('start')
.mockReturnValue(isStart)
.calledWith('userCancel')
.mockReturnValue(false)
.calledWith('waitForDOMAndCameraDetails')
.mockReturnValue(false)
.calledWith('detectFaceBeforeStart')
.mockReturnValue(false)
.calledWith('recording')
.mockReturnValue(isRecording);
.mockReturnValue(isRecording)
.calledWith('checkSucceeded')
.mockReturnValue(false)
.calledWith({ recording: 'flashFreshnessColors' })
.mockReturnValue(false);
}

beforeEach(() => {
Expand All @@ -109,6 +152,7 @@ describe('LivenessCameraModule', () => {
videoHeight: 100,
videoWidth: 100,
});
mockGetUserMedia.mockResolvedValue(mockMediaStream);
drawStaticOvalSpy.mockClear();
(global.navigator.mediaDevices as any) = {
getUserMedia: jest.fn(),
Expand Down Expand Up @@ -736,4 +780,80 @@ describe('LivenessCameraModule', () => {
});
expect(drawStaticOvalSpy).toHaveBeenCalledTimes(1);
});
describe('Camera Device Switching', () => {
it('should call getUserMedia with correct constraints when camera changes', async () => {
// Reset all mocks before test
jest.clearAllMocks();
isStart = true;
mockStateMatchesAndSelectors();

const newDeviceId = 'new-camera-device-id';
const mockGetUserMedia = jest
.fn()
.mockResolvedValue(mockVideoMediaStream);

// Mock navigator.mediaDevices using Object.defineProperty
Object.defineProperty(global.navigator, 'mediaDevices', {
value: {
getUserMedia: mockGetUserMedia,
enumerateDevices: jest.fn(),
},
writable: true,
});

// Mock the selectors to return multiple devices and video constraints
mockUseLivenessSelector.mockImplementation((selector) => {
if (selector === selectSelectableDevices) {
return [
{ deviceId: 'device-1', label: 'Camera 1' },
{ deviceId: newDeviceId, label: 'Camera 2' },
];
}
if (selector === selectSelectedDeviceId) {
return 'device-1';
}
if (selector === selectVideoConstraints) {
return mockVideoConstraints;
}
return undefined;
});

await waitFor(() => {
renderWithLivenessProvider(
<LivenessCameraModule
isMobileScreen={false}
isRecordingStopped={false}
hintDisplayText={hintDisplayText}
streamDisplayText={streamDisplayText}
errorDisplayText={errorDisplayText}
cameraDisplayText={cameraDisplayText}
instructionDisplayText={instructionDisplayText}
/>
);
});

const videoEl = screen.getByTestId('video');
await waitFor(() => {
videoEl.dispatchEvent(new Event('loadedmetadata'));
});

// Find the camera selector dropdown
const cameraSelector = screen.getByRole('combobox') as HTMLSelectElement;
expect(cameraSelector).toBeInTheDocument();

// Simulate changing the camera selection
fireEvent.change(cameraSelector, { target: { value: newDeviceId } });

// Verify getUserMedia was called with the correct constraints
await waitFor(() => {
expect(mockGetUserMedia).toHaveBeenCalledWith({
video: {
...mockVideoConstraints,
deviceId: { exact: newDeviceId },
},
audio: false,
});
});
});
});
});
Loading
Loading