Skip to content

Commit

Permalink
fix: make mediaWidth/height logic more device agnostic (#727)
Browse files Browse the repository at this point in the history
  • Loading branch information
hbuchel committed Feb 10, 2023
1 parent 9035050 commit dcc5b82
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
useLivenessSelector,
createLivenessSelector,
useMediaStreamInVideo,
useMediaDimensions,
} from '../hooks';
import { CancelButton, Instruction, RecordingIcon, Overlay } from '../shared';
import { isFirefox, isAndroid, isIOS } from '../utils/device';
import { Flex, Loader, Text, View } from '../../../primitives';
import { LivenessClassNames } from '../types/classNames';

Expand Down Expand Up @@ -41,6 +41,12 @@ export const LivenessCameraModule = (
videoStream,
videoConstraints
);

const { width: mediaWidth, height: mediaHeight } = useMediaDimensions(
videoWidth,
videoHeight
);

const canvasRef = useRef<HTMLCanvasElement>(null);
const freshnessColorRef = useRef<HTMLDivElement | null>(null);

Expand All @@ -52,15 +58,6 @@ export const LivenessCameraModule = (
const isRecording = state.matches('recording');
const isCheckSucceeded = state.matches('checkSucceeded');

/**
* Temp fix: Firefox on Android + iOS returns opposite values you'd expect
* from getUserMedia().
*/
const shouldFlipValues = (isAndroid() && isFirefox()) || isIOS();

const mediaHeight = shouldFlipValues ? videoWidth : videoHeight;
const mediaWidth = shouldFlipValues ? videoHeight : videoWidth;

React.useLayoutEffect(() => {
if (isCameraReady) {
send({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { when, resetAllWhenMocks } from 'jest-when';
import {
renderWithLivenessProvider,
getMockedFunction,
mockMatchMedia,
} from '../../utils/test-utils';
import {
useLivenessActor,
useLivenessSelector,
useMediaStreamInVideo,
useMediaDimensions,
} from '../../hooks';
import {
LivenessCameraModule,
Expand All @@ -25,6 +27,7 @@ jest.mock('../../shared/Instruction');
const mockUseLivenessActor = getMockedFunction(useLivenessActor);
const mockUseLivenessSelector = getMockedFunction(useLivenessSelector);
const mockUseMediaStreamInVideo = getMockedFunction(useMediaStreamInVideo);
const mockUseMediaDimensions = getMockedFunction(useMediaDimensions);

describe('LivenessCameraModule', () => {
const mockActorState: any = {
Expand All @@ -47,13 +50,18 @@ describe('LivenessCameraModule', () => {
}

beforeEach(() => {
mockMatchMedia();
mockUseLivenessActor.mockReturnValue([mockActorState, mockActorSend]);
mockUseLivenessSelector.mockReturnValueOnce({}).mockReturnValueOnce({});
mockUseMediaStreamInVideo.mockReturnValue({
videoRef: { current: document.createElement('video') },
videoHeight: 100,
videoWidth: 100,
});
mockUseMediaDimensions.mockReturnValue({
width: 100,
height: 100,
});
});

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { render, screen } from '@testing-library/react';
import { useActor } from '@xstate/react';

import { FaceLivenessDetector, FaceLivenessDetectorProps } from '..';
import { getMockedFunction } from '../utils/test-utils';
import { getMockedFunction, mockMatchMedia } from '../utils/test-utils';
import { getVideoConstraints } from '../StartLiveness/helpers';
import { useMediaStreamInVideo, useLivenessActor } from '../hooks';
import {
useMediaStreamInVideo,
useLivenessActor,
useMediaDimensions,
} from '../hooks';

jest.mock('../../../styles.css', () => ({}));
jest.mock('@xstate/react');
Expand All @@ -16,6 +20,7 @@ const mockUseActor = getMockedFunction(useActor);
const mockUseLivenessActor = getMockedFunction(useLivenessActor);
const mockGetVideoConstraints = getMockedFunction(getVideoConstraints);
const mockUseMediaStreamInVideo = getMockedFunction(useMediaStreamInVideo);
const mockUseMediaDimensions = getMockedFunction(useMediaDimensions);
const mockMatches = jest.fn().mockImplementation(() => {
return true;
});
Expand All @@ -33,6 +38,10 @@ describe('FaceLivenessDetector', () => {
videoHeight: 100,
videoWidth: 100,
});
mockUseMediaDimensions.mockReturnValue({
width: 100,
height: 100,
});

const mockVideoConstraints = {};
mockGetVideoConstraints.mockReturnValue(mockVideoConstraints);
Expand All @@ -47,6 +56,10 @@ describe('FaceLivenessDetector', () => {
const livenessTestId = 'liveness-detector';
const livenessCheckTestId = 'liveness-detector-check';

beforeAll(() => {
mockMatchMedia();
});

afterEach(() => {
jest.clearAllMocks();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { renderHook } from '@testing-library/react-hooks';

import { useMediaDimensions } from '../useMediaDimensions';
import { getMockedFunction } from '../../utils/test-utils';
import { isMobileScreen, isPortrait } from '../../utils/device';

jest.mock('../../utils/device');
const mockIsMobileScreen = getMockedFunction(isMobileScreen);
const mockIsPortrait = getMockedFunction(isPortrait);

describe('useMediaDimensions mobile portrait', () => {
beforeEach(() => {
mockIsMobileScreen.mockReturnValue(true);
mockIsPortrait.mockReturnValue(true);
});
it('should return portrait sized dimensions when width is less than height', () => {
const videoWidth = 360;
const videoHeight = 640;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(360);
expect(result.current.height).toBe(640);
});
it('should return portrait sized dimensions when width is greater than height', () => {
const videoWidth = 640;
const videoHeight = 360;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(360);
expect(result.current.height).toBe(640);
});
});

describe('useMediaDimensions mobile landscape', () => {
beforeEach(() => {
mockIsMobileScreen.mockReturnValue(true);
mockIsPortrait.mockReturnValue(false);
});
it('should return landscape sized dimensions when width is greater than height', () => {
const videoWidth = 640;
const videoHeight = 480;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(640);
expect(result.current.height).toBe(480);
});
it('should return portrait sized dimensions when width is less than height', () => {
const videoWidth = 480;
const videoHeight = 640;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(640);
expect(result.current.height).toBe(480);
});
});

describe('useMediaDimensions desktop', () => {
it('should return videoWidth and videoHeight as width and height on portrait', () => {
mockIsMobileScreen.mockReturnValue(false);
mockIsPortrait.mockReturnValue(true);

const videoWidth = 640;
const videoHeight = 480;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(videoWidth);
expect(result.current.height).toBe(videoHeight);
});

it('should return videoWidth and videoHeight as width and height on landscape', () => {
mockIsMobileScreen.mockReturnValue(false);
mockIsPortrait.mockReturnValue(true);

const videoWidth = 480;
const videoHeight = 640;

const { result } = renderHook(() => {
return useMediaDimensions(videoWidth, videoHeight);
});

expect(result.current.width).toBe(videoWidth);
expect(result.current.height).toBe(videoHeight);
});
});
1 change: 1 addition & 0 deletions packages/react/src/components/Liveness/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useLivenessActor';
export * from './useLivenessSelector';
export * from './useMediaStreamInVideo';
export * from './useMediaDimensions';
43 changes: 43 additions & 0 deletions packages/react/src/components/Liveness/hooks/useMediaDimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useLayoutEffect, useState } from 'react';
import { isPortrait, isMobileScreen } from '../utils/device';

interface UseMediaDimensionsResults {
width: number;
height: number;
}

/**
* Calculate video dimensions based on the width and height values
* supplied in LivenessCameraModule from getUserMedia.
* In our testing, some devices and browser combos on mobile return
* opposite values than you would expect from getUserMedia so we need
* to manually test dimensions before setting the mediaWidth/Height
* for portrait and landscape.
* Known affected browsers:
* - Firefox on Android
* - iOS
*/
export function useMediaDimensions(
videoWidth: number,
videoHeight: number
): UseMediaDimensionsResults {
const [width, setWidth] = useState<number>(videoWidth);
const [height, setHeight] = useState<number>(videoHeight);

useLayoutEffect(() => {
if (isMobileScreen()) {
if (isPortrait()) {
setWidth(videoWidth < videoHeight ? videoWidth : videoHeight);
setHeight(videoWidth < videoHeight ? videoHeight : videoWidth);
} else {
setWidth(videoWidth > videoHeight ? videoWidth : videoHeight);
setHeight(videoWidth > videoHeight ? videoHeight : videoWidth);
}
} else {
setWidth(videoWidth);
setHeight(videoHeight);
}
}, [videoWidth, videoHeight]);

return { width, height };
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
isAndroid,
isFirefox,
isIOS,
isMobileScreen,
isPortrait,
} from '../device';
import { isAndroid, isIOS, isMobileScreen, isPortrait } from '../device';

const GOOGLE_PIXEL_FIREFOX =
'Mozilla/5.0 (Linux; Android 12; Pixel 6 Build/SD1A.210817.023; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Firefox/94.0.4606.71 Mobile Safari/537.36';
Expand Down Expand Up @@ -88,12 +82,4 @@ describe('device', () => {
(global.navigator as any).userAgent = IPHONE_12_SAFARI;
expect(isAndroid()).toBe(false);
});

it('isFirefox', () => {
(global.navigator as any).userAgent = GOOGLE_PIXEL_CHROME;
expect(isFirefox()).toBe(false);

(global.navigator as any).userAgent = GOOGLE_PIXEL_FIREFOX;
expect(isFirefox()).toBe(true);
});
});
3 changes: 0 additions & 3 deletions packages/react/src/components/Liveness/utils/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ export function isIOS(): boolean {
export function isAndroid(): boolean {
return navigator.userAgent.indexOf('Android') != -1;
}
export function isFirefox(): boolean {
return navigator.userAgent.indexOf('Firefox') != -1;
}

export function isPortrait(): boolean {
return window.matchMedia('(orientation: portrait)').matches;
Expand Down
17 changes: 17 additions & 0 deletions packages/react/src/components/Liveness/utils/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,20 @@ export function getMockedFunction<T extends (...args: any[]) => any>(
): jest.MockedFunction<T> {
return fn as jest.MockedFunction<T>;
}

export function mockMatchMedia(
mediaQuery: string = '',
matches: boolean = false
): void {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query: string = mediaQuery) => ({
matches: matches,
media: query,
onchange: null,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}

0 comments on commit dcc5b82

Please sign in to comment.