Skip to content

Commit

Permalink
Fixed the stream dimensions logic in the Camera component
Browse files Browse the repository at this point in the history
  • Loading branch information
souyahia-monk committed Apr 24, 2024
1 parent 21a7ecc commit 9474a3d
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 104 deletions.
8 changes: 7 additions & 1 deletion packages/camera-web/src/Camera/Camera.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useMemo } from 'react';
import { AllOrNone, RequiredKeys } from '@monkvision/types';
import { isMobileDevice } from '@monkvision/common';
import {
CameraFacingMode,
CameraResolution,
Expand Down Expand Up @@ -97,14 +98,18 @@ export function Camera<T extends object>({
monitoring,
onPictureTaken,
}: CameraProps<T>) {
const previewResolution = useMemo(
() => (isMobileDevice() ? CameraResolution.UHD_4K : CameraResolution.FHD_1080P),
[],
);
const {
ref: videoRef,
dimensions: streamDimensions,
error,
retry,
isLoading: isPreviewLoading,
} = useCameraPreview({
resolution: CameraResolution.UHD_4K,
resolution: previewResolution,
facingMode: CameraFacingMode.ENVIRONMENT,
});
const { ref: canvasRef, dimensions: canvasDimensions } = useCameraCanvas({
Expand Down Expand Up @@ -134,6 +139,7 @@ export function Camera<T extends object>({
autoPlay
playsInline={true}
controls={false}
muted={true}
data-testid='camera-video-preview'
/>
<canvas ref={canvasRef} style={styles['cameraCanvas']} data-testid='camera-canvas' />
Expand Down
2 changes: 1 addition & 1 deletion packages/camera-web/src/Camera/hooks/useCameraPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface CameraPreviewHandle extends UserMediaResult {
export function useCameraPreview(config: CameraConfig): CameraPreviewHandle {
const ref = useRef<HTMLVideoElement>(null);
const { handleError } = useMonitoring();
const userMediaResult = useUserMedia(getMediaConstraints(config));
const userMediaResult = useUserMedia(getMediaConstraints(config), ref);

useEffect(() => {
if (userMediaResult.stream && ref.current) {
Expand Down
62 changes: 33 additions & 29 deletions packages/camera-web/src/Camera/hooks/useUserMedia.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMonitoring } from '@monkvision/monitoring';
import deepEqual from 'fast-deep-equal';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { RefObject, useCallback, useEffect, useMemo, useState } from 'react';
import { PixelDimensions } from '@monkvision/types';
import { isMobileDevice } from '@monkvision/common';
import { getValidCameraDeviceIds } from './utils';
Expand Down Expand Up @@ -111,7 +111,14 @@ export interface UserMediaResult {
retry: () => void;
}

function getStreamDimensions(stream: MediaStream): PixelDimensions {
function swapDimensions(dimensions: PixelDimensions): PixelDimensions {
return {
width: dimensions.height,
height: dimensions.width,
};
}

function getStreamDimensions(stream: MediaStream, checkOrientation: boolean): PixelDimensions {
const videoTracks = stream.getVideoTracks();
if (videoTracks.length === 0) {
throw new InvalidStreamError(
Expand All @@ -132,14 +139,14 @@ function getStreamDimensions(stream: MediaStream): PixelDimensions {
InvalidStreamErrorName.NO_DIMENSIONS,
);
}
return { width, height };
}
const dimensions = { width, height };
if (!isMobileDevice() || !checkOrientation) {
return dimensions;
}

function swapWidthAndHeight(dimensions: PixelDimensions): PixelDimensions {
return {
width: dimensions.height,
height: dimensions.width,
};
const isStreamInPortrait = width < height;
const isDeviceInPortrait = window.matchMedia('(orientation: portrait)').matches;
return isStreamInPortrait !== isDeviceInPortrait ? swapDimensions(dimensions) : dimensions;
}

/**
Expand All @@ -152,12 +159,16 @@ function swapWidthAndHeight(dimensions: PixelDimensions): PixelDimensions {
*
* @param constraints The same media constraints you would pass to the `getUserMedia` function. Note that this hook has
* been designed for video only, so audio constraints could provoke unexpected behaviour.
* @param videoRef The ref to the video element displaying the camera preview stream.
* @return The result of this hook contains the resulting video stream, an error object if there has been an error, a
* loading indicator and a retry function that tries to get a camera stream again. See the `UserMediaResult` interface
* for more information.
* @see UserMediaResult
*/
export function useUserMedia(constraints: MediaStreamConstraints): UserMediaResult {
export function useUserMedia(
constraints: MediaStreamConstraints,
videoRef: RefObject<HTMLVideoElement>,
): UserMediaResult {
const [stream, setStream] = useState<MediaStream | null>(null);
const [dimensions, setDimensions] = useState<PixelDimensions | null>(null);
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -215,21 +226,15 @@ export function useUserMedia(constraints: MediaStreamConstraints): UserMediaResu
const updatedConstraints = {
...constraints,
video: {
...(constraints ? (constraints.video as MediaStreamConstraints) : null),
...(constraints ? (constraints.video as MediaTrackConstraints) : {}),
deviceId: { exact: cameraDeviceIds },
},
};
const str = await navigator.mediaDevices.getUserMedia(updatedConstraints);
str?.addEventListener('inactive', onStreamInactive);
setStream(str);

const dimensionsStr = getStreamDimensions(str);
const isPortrait = window.matchMedia('(orientation: portrait)').matches;
setDimensions(
dimensionsStr.width > dimensionsStr.height && isMobileDevice() && isPortrait
? swapWidthAndHeight(dimensionsStr)
: dimensionsStr,
);
setDimensions(getStreamDimensions(str, true));
setIsLoading(false);
} catch (err) {
handleGetUserMediaError(err);
Expand All @@ -240,18 +245,17 @@ export function useUserMedia(constraints: MediaStreamConstraints): UserMediaResu
}, [constraints, stream, error, isLoading, lastConstraintsApplied]);

useEffect(() => {
const portrait = window.matchMedia('(orientation: portrait)');

const handleOrientationChange = () => {
if (stream) {
const dimensionsStr = getStreamDimensions(stream);
setDimensions(isMobileDevice() ? swapWidthAndHeight(dimensionsStr) : dimensionsStr);
}
};
portrait.addEventListener('change', handleOrientationChange);

let isActive = true;
if (stream && videoRef.current) {
// eslint-disable-next-line no-param-reassign
videoRef.current.onresize = () => {
if (isActive) {
setDimensions(getStreamDimensions(stream, false));
}
};
}
return () => {
portrait.removeEventListener('change', handleOrientationChange);
isActive = false;
};
}, [stream]);

Expand Down
40 changes: 35 additions & 5 deletions packages/camera-web/test/Camera/Camera.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, { createRef } from 'react';

Object.defineProperty(HTMLMediaElement.prototype, 'muted', {
set: () => {},
});

jest.mock('../../src/Camera/hooks', () => ({
...jest.requireActual('../../src/Camera/hooks'),
useCameraPreview: jest.fn(() => ({
Expand Down Expand Up @@ -35,6 +39,7 @@ import {
useCompression,
useTakePicture,
} from '../../src/Camera/hooks';
import { isMobileDevice } from '@monkvision/common';

const VIDEO_PREVIEW_TEST_ID = 'camera-video-preview';
const CANVAS_TEST_ID = 'camera-canvas';
Expand All @@ -44,13 +49,38 @@ describe('Camera component', () => {
jest.clearAllMocks();
});

it('should pass the proper props to the useCameraPreview hook', () => {
it('should ask for an environment camera for the camera preview', () => {
const { unmount } = render(<Camera resolution={CameraResolution.HD_720P} />);

expect(useCameraPreview).toHaveBeenCalledWith({
facingMode: CameraFacingMode.ENVIRONMENT,
resolution: CameraResolution.UHD_4K,
});
expect(useCameraPreview).toHaveBeenCalledWith(
expect.objectContaining({
facingMode: CameraFacingMode.ENVIRONMENT,
}),
);
unmount();
});

it('should ask for a 4K camera for the camera preview on mobile devices', () => {
(isMobileDevice as jest.Mock).mockImplementationOnce(() => true);
const { unmount } = render(<Camera resolution={CameraResolution.HD_720P} />);

expect(useCameraPreview).toHaveBeenCalledWith(
expect.objectContaining({
resolution: CameraResolution.UHD_4K,
}),
);
unmount();
});

it('should ask for a FHD camera for the camera preview on desktop devices', () => {
(isMobileDevice as jest.Mock).mockImplementationOnce(() => false);
const { unmount } = render(<Camera resolution={CameraResolution.HD_720P} />);

expect(useCameraPreview).toHaveBeenCalledWith(
expect.objectContaining({
resolution: CameraResolution.FHD_1080P,
}),
);
unmount();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ describe('useCameraPreview hook', () => {
unmount();
});

it('should make a call to useUserMedia with constraints obtained from useUserMedia', async () => {
const { unmount } = renderHook(useCameraPreview);
it('should make a call to useUserMedia with constraints obtained from useUserMedia and the video ref', async () => {
const { result, unmount } = renderHook(useCameraPreview);
await waitFor(() => {
expect(useUserMedia).toHaveBeenCalledWith((getMediaConstraints as jest.Mock)());
expect(useUserMedia).toHaveBeenCalledWith(
(getMediaConstraints as jest.Mock)(),
result.current.ref,
);
});
unmount();
});
Expand Down
Loading

0 comments on commit 9474a3d

Please sign in to comment.