diff --git a/apps/monk-test-app/src/views/TestView/TestView.css b/apps/monk-test-app/src/views/TestView/TestView.css index 1d6e023db..ac2ec2d04 100644 --- a/apps/monk-test-app/src/views/TestView/TestView.css +++ b/apps/monk-test-app/src/views/TestView/TestView.css @@ -7,3 +7,10 @@ justify-content: center; color: white; } + +.select-container { + position: fixed; + top: 50px; + left: 50px; + z-index: 9999; +} diff --git a/apps/monk-test-app/src/views/TestView/TestView.tsx b/apps/monk-test-app/src/views/TestView/TestView.tsx index b93cda0ff..9a8740219 100644 --- a/apps/monk-test-app/src/views/TestView/TestView.tsx +++ b/apps/monk-test-app/src/views/TestView/TestView.tsx @@ -1,52 +1,39 @@ -import React, { useState } from 'react'; -import { PhotoCapture } from '@monkvision/inspection-capture-web'; -import { sights } from '@monkvision/sights'; +import { Camera, CameraResolution, MonkPicture, SimpleCameraHUD } from '@monkvision/camera-web'; import './TestView.css'; - -const captureSights = [ - sights['haccord-8YjMcu0D'], - sights['haccord-DUPnw5jj'], - sights['haccord-hsCc_Nct'], - sights['haccord-GQcZz48C'], - sights['haccord-QKfhXU7o'], - sights['haccord-mdZ7optI'], - sights['haccord-bSAv3Hrj'], - sights['haccord-W-Bn3bU1'], - sights['haccord-GdWvsqrm'], - sights['haccord-ps7cWy6K'], - sights['haccord-Jq65fyD4'], - sights['haccord-OXYy5gET'], - sights['haccord-5LlCuIfL'], - sights['haccord-Gtt0JNQl'], - sights['haccord-cXSAj2ez'], - sights['haccord-KN23XXkX'], - sights['haccord-Z84erkMb'], -]; - -const inspectionId = 'b072ff42-6244-ef3b-b018-5d3d6562c1bd'; - -const apiConfig = { - apiDomain: 'api.preview.monk.ai/v1', - authToken: - 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjNLUnpaNm01WDFzOWFBZWRudnBrWSJ9.eyJpc3MiOiJodHRwczovL2lkcC5wcmV2aWV3Lm1vbmsuYWkvIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMDY5MzYxMTEwMDU4MDYxODA1NTYiLCJhdWQiOlsiaHR0cHM6Ly9hcGkubW9uay5haS92MS8iLCJodHRwczovL21vbmstcHJldmlldy5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNzA3NDcxNzU5LCJleHAiOjE3MDc0Nzg5NTksImF6cCI6InNvWjdQMmM2YjlJNWphclFvUnJoaDg3eDlUcE9TYUduIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsInBlcm1pc3Npb25zIjpbIm1vbmtfY29yZV9hcGk6Y29tcGxpYW5jZXMiLCJtb25rX2NvcmVfYXBpOmRhbWFnZV9kZXRlY3Rpb24iLCJtb25rX2NvcmVfYXBpOmRhc2hib2FyZF9vY3IiLCJtb25rX2NvcmVfYXBpOmltYWdlc19vY3IiLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOmNyZWF0ZSIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6ZGVsZXRlIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpkZWxldGVfYWxsIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpkZWxldGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpyZWFkIiwibW9ua19jb3JlX2FwaTppbnNwZWN0aW9uczpyZWFkX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6cmVhZF9vcmdhbml6YXRpb24iLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOnVwZGF0ZSIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6dXBkYXRlX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6dXBkYXRlX29yZ2FuaXphdGlvbiIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6d3JpdGUiLCJtb25rX2NvcmVfYXBpOmluc3BlY3Rpb25zOndyaXRlX2FsbCIsIm1vbmtfY29yZV9hcGk6aW5zcGVjdGlvbnM6d3JpdGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTpyZXBhaXJfZXN0aW1hdGUiLCJtb25rX2NvcmVfYXBpOnVzZXJzOnJlYWQiLCJtb25rX2NvcmVfYXBpOnVzZXJzOnJlYWRfYWxsIiwibW9ua19jb3JlX2FwaTp1c2VyczpyZWFkX29yZ2FuaXphdGlvbiIsIm1vbmtfY29yZV9hcGk6dXNlcnM6dXBkYXRlIiwibW9ua19jb3JlX2FwaTp1c2Vyczp1cGRhdGVfYWxsIiwibW9ua19jb3JlX2FwaTp1c2Vyczp1cGRhdGVfb3JnYW5pemF0aW9uIiwibW9ua19jb3JlX2FwaTp1c2Vyczp3cml0ZSIsIm1vbmtfY29yZV9hcGk6dXNlcnM6d3JpdGVfYWxsIiwibW9ua19jb3JlX2FwaTp1c2Vyczp3cml0ZV9vcmdhbml6YXRpb24iLCJtb25rX2NvcmVfYXBpOndoZWVsX2FuYWx5c2lzIl19.m38G5NaCD_pcGptJdLPa_TVtD08yRY9qdp-5pCELd8Ekzma0kCWMctwHvlrv2OsjynlXyAutIK2uhDMrdLdnPk_6bU4rZheej2s0obXaXqZCUTbUOh8scL-81tbH_ZlKN3oSXfqUVMnwvpa1bnXZHmjeHi2e3bhvjxW-Jg5DrBB9gNfstxK0hugrlxtNL96y6ImEITOxOMEbURYGwOLQQtLkRqFo7AeZCu-_w6UbtFZfLJc5FlsWpTKy7I3_xMynzDGGaADRQeyazL_7DfemCb_VPXkR9aV67tF4pt6jevCetYI5CfN5X2JJrp7XNES_M2d7wGdJl5C6lbBPs3n3SA', -}; +import { useState } from 'react'; export function TestView() { - const [complete, setComplete] = useState(false); + const [resolution, setResolution] = useState(CameraResolution.UHD_4K); + + const handlePictureTaken = (picture: MonkPicture) => { + const link = document.createElement('a'); + link.href = picture.uri; + const now = new Date(); + link.download = `pic-${resolution}-${now.getHours()}-${now.getMinutes()}-${now.getSeconds()}.jpg`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; return (
- {complete ? ( - 'Inspection Complete!' - ) : ( - setComplete(true)} - onClose={() => console.log('coucou')} - /> - )} + +
+ +
); } diff --git a/packages/public/camera-web/README.md b/packages/public/camera-web/README.md index 52772bcd7..2f771c445 100644 --- a/packages/public/camera-web/README.md +++ b/packages/public/camera-web/README.md @@ -33,13 +33,9 @@ function MyCameraPreview() { } ``` -## Camera constraints -The resolution quality of the camera of the Camera video stream that is fetched from the user's device is configurable -by passing it as a prop to the Camera component. Note that device selection (selecting which Camera will be used when -the device has many available) is disabled for now. This is because this is instead handled automatically by the -component in order to prevent unusable cameras (zoomed ones for instance) to be used. - -Example of how to configure the resolution of the Camera : +## Camera resolution +The resolution quality of the pictures taken by the Camera component is configurable by passing it as a prop to the +Camera component : ```tsx import { Camera, CameraResolution } from '@monkvision/camera-web'; @@ -49,20 +45,15 @@ function MyCameraPreview() { } ``` -For more details on the camera constraints options, see the *API* section below. - Notes : -- When the camera constraints are updated, the video stream is automatically updated without asking the user for - permissions another time -- Only the resolutions available in the `CameraResolution` enum are allowed for better results with our AI models -- The resolutions available in the `CameraResolution` enum are all in the 16:9 format -- Device selection (selecting which Camera will be used when the device has many available) is disabled for now. This is - because this is instead handled automatically by the component in order to prevent unusable cameras (zoomed ones for - instance) to be used. -- If no device meets the given requirements, the device with the closest match will be used : - - If the needed resolution is too high, the highest resolution will be used : this means that asking for the - `CameraResolution.UHD_4K` resolution is a good way to get the highest resolution possible - - If the needed resolution is too low the browser will crop and scale the camera's feed to meet the requirements +- This option does not affect the resolution of the Camera preview : the preview will always use the highest + resolution available on the current device. +- If the specified resolution is not equal to the one used by the device's native camera, the pictures taken will be + scaled to fit the requirements. +- The resolutions available in the `CameraResolution` enum are all in the 16:9 format. +- If the aspect ratio of the specified resolution differs from the one of the device's camera, pictures taken will + always have the same aspect ratio as the native camera one, and will be scaled in a way to make sure that neither the + width, nor the height of the output picture will exceed the dimensions of the specified resolution. ## Compression options When pictures are taken by the camera, they are compressed and encoded. The compression format and quality can be @@ -169,7 +160,7 @@ Main component exported by this package, displays a Camera preview and the given ### Props | Prop | Type | Description | Required | Default Value | |----------------|--------------------------------|---------------------------------------------------------------------------------------------------------------------------------|----------|--------------------------------| -| resolution | CameraResolution | Resolution of the camera to use. | | `CameraResolution.UHD_4K` | +| resolution | CameraResolution | Resolution of the pictures taken by the camera. This does not affect the resolution of the Camera preview. | | `CameraResolution.UHD_4K` | | format | CompressionFormat | The compression format used to compress images taken by the camera. | | `CompressionFormat.JPEG` | | quality | number | The image quality when using a compression format that supports lossy compression. From 0 (lowest quality) to 1 (best quality). | | `0.8` | | HUDComponent | CameraHUDComponent | The camera HUD component to display on top of the camera preview. | | | diff --git a/packages/public/camera-web/src/Camera/Camera.tsx b/packages/public/camera-web/src/Camera/Camera.tsx index 8d2f77177..4e97d2121 100644 --- a/packages/public/camera-web/src/Camera/Camera.tsx +++ b/packages/public/camera-web/src/Camera/Camera.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { AllOrNone, RequiredKeys } from '@monkvision/types'; import { - CameraConfig, CameraFacingMode, CameraResolution, CompressionFormat, @@ -47,10 +46,31 @@ export type HUDConfigProps = RequiredKeys extends never /** * Props given to the Camera component. The generic T type corresponds to the prop types of the HUD. */ -export type CameraProps = Partial> & - Partial & +export type CameraProps = Partial & CameraEventHandlers & HUDConfigProps & { + /** + * This option specifies the resolution of the pictures taken by the Camera. This option does not affect the + * resolution of the Camera preview (it will always be the highest resolution possible). If the specified resolution + * is not equal to the one used by the device's native camera, the pictures taken will be scaled to fit the + * requirements. Note that if the aspect ratio of the specified resolution differs from the one of the device's + * camera, pictures taken will always have the same aspect ratio as the native camera one, and will be scaled in a way + * to make sure that neither the width, nor the height of the output picture will exceed the dimensions of the + * specified resolution. + * + * Note: If the specified resolution is higher than the best resolution available on the current device, output + * pictures will only be scaled up to the specified resolution if the `allowImageUpscaling` property is set to `true`. + * + * @default `CameraResolution.UHD_4K` + */ + resolution?: CameraResolution; + /** + * When the native resolution of the device Camera is smaller than the resolution asked in the `resolution` prop, + * resulting pictures will only be scaled up if this property is set to `true`. + * + * @default `false` + */ + allowImageUpscaling?: boolean; /** * Additional monitoring config that can be provided to the Camera component. */ @@ -71,6 +91,7 @@ export function Camera({ resolution = CameraResolution.UHD_4K, format = CompressionFormat.JPEG, quality = 0.8, + allowImageUpscaling = false, HUDComponent, hudProps, monitoring, @@ -78,13 +99,24 @@ export function Camera({ }: CameraProps) { const { ref: videoRef, - dimensions, + dimensions: streamDimensions, error, retry, isLoading: isPreviewLoading, - } = useCameraPreview({ resolution, facingMode: CameraFacingMode.ENVIRONMENT }); - const { ref: canvasRef } = useCameraCanvas({ dimensions }); - const { takeScreenshot } = useCameraScreenshot({ videoRef, canvasRef, dimensions }); + } = useCameraPreview({ + resolution: CameraResolution.UHD_4K, + facingMode: CameraFacingMode.ENVIRONMENT, + }); + const { ref: canvasRef, dimensions: canvasDimensions } = useCameraCanvas({ + resolution, + streamDimensions, + allowImageUpscaling, + }); + const { takeScreenshot } = useCameraScreenshot({ + videoRef, + canvasRef, + dimensions: canvasDimensions, + }); const { compress } = useCompression({ canvasRef, options: { format, quality } }); const { takePicture, isLoading: isTakePictureLoading } = useTakePicture({ compress, @@ -112,7 +144,7 @@ export function Camera({ return HUDComponent ? ( diff --git a/packages/public/camera-web/src/Camera/hooks/useCameraCanvas.ts b/packages/public/camera-web/src/Camera/hooks/useCameraCanvas.ts index a00ad6151..98793f59c 100644 --- a/packages/public/camera-web/src/Camera/hooks/useCameraCanvas.ts +++ b/packages/public/camera-web/src/Camera/hooks/useCameraCanvas.ts @@ -1,14 +1,24 @@ -import { RefObject, useEffect, useRef } from 'react'; +import { RefObject, useEffect, useMemo, useRef } from 'react'; import { PixelDimensions } from '@monkvision/types'; +import { CameraResolution, getResolutionDimensions } from './utils'; /** * Object used to configure the camera canvas. */ export interface CameraCanvasConfig { + /** + * The resolution of the pictures taken asked by the user of the Camera component. + */ + resolution: CameraResolution; + /** + * Boolean indicating if the Camera component should allow image upscaling when the asked resolution is bigger than + * the one of the device Camera. + */ + allowImageUpscaling: boolean; /** * The dimensions of the video stream. */ - dimensions: PixelDimensions | null; + streamDimensions: PixelDimensions | null; } /** @@ -19,13 +29,65 @@ export interface CameraCanvasHandle { * The ref to the canvas element. Forward this ref to the tag to set it up. */ ref: RefObject; + /** + * The dimensions of the canvas. + */ + dimensions: PixelDimensions | null; +} + +/** + * This function is used to calculate the dimensions of the canvas that will be used to draw the image, thus also + * calculating the output dimensions of the image itself, respecting the following logic : + * - If the aspect ratio of the stream and constraints are the same, we simply scale the stream to make it fit the + * constraints. Note that if `allowImageUpscaling` is `false`, and the stream is smaller than the constraints, we don't + * scale "up" the stream image, and we simply return the stream dimensions. + * - If the aspect ratio of the stream is different from the one specified in the constraints, the logic is the same, + * but the output aspect ratio will be the same one as the stream. The stream dimensions will simply be scaled + * following the same logic as the previous point, making sure that neither the width nor the height of the canvas will + * exceed the ones described by the constraints. + */ +function getCanvasDimensions({ + resolution, + streamDimensions, + allowImageUpscaling, +}: CameraCanvasConfig): PixelDimensions | null { + if (!streamDimensions) { + return null; + } + const isPortrait = streamDimensions.width < streamDimensions.height; + const constraintsDimensions = getResolutionDimensions(resolution, isPortrait); + const streamRatio = streamDimensions.width / streamDimensions.height; + + if ( + constraintsDimensions.width > streamDimensions.width && + constraintsDimensions.height > streamDimensions.height && + !allowImageUpscaling + ) { + return { + width: streamDimensions.width, + height: streamDimensions.height, + }; + } + const fitToHeight = constraintsDimensions.width / streamRatio > constraintsDimensions.height; + return { + width: fitToHeight ? constraintsDimensions.height * streamRatio : constraintsDimensions.width, + height: fitToHeight ? constraintsDimensions.height : constraintsDimensions.width / streamRatio, + }; } /** * Custom hook used to manage the camera element used to take video screenshots and encode images. */ -export function useCameraCanvas({ dimensions }: CameraCanvasConfig): CameraCanvasHandle { +export function useCameraCanvas({ + resolution, + streamDimensions, + allowImageUpscaling, +}: CameraCanvasConfig): CameraCanvasHandle { const ref = useRef(null); + const dimensions = useMemo( + () => getCanvasDimensions({ resolution, streamDimensions, allowImageUpscaling }), + [resolution, streamDimensions], + ); useEffect(() => { if (dimensions && ref.current) { @@ -34,5 +96,5 @@ export function useCameraCanvas({ dimensions }: CameraCanvasConfig): CameraCanva } }, [dimensions]); - return { ref }; + return { ref, dimensions }; } diff --git a/packages/public/camera-web/src/Camera/hooks/utils/getMediaContraints.ts b/packages/public/camera-web/src/Camera/hooks/utils/getMediaContraints.ts index 72bb6f4bc..ec80e2ccd 100644 --- a/packages/public/camera-web/src/Camera/hooks/utils/getMediaContraints.ts +++ b/packages/public/camera-web/src/Camera/hooks/utils/getMediaContraints.ts @@ -1,3 +1,5 @@ +import { PixelDimensions } from '@monkvision/types'; + /** * Enumeration of the facing modes for the camera constraints. */ @@ -50,8 +52,8 @@ export enum CameraResolution { UHD_4K = '4K', } -const CAMERA_RESOLUTION_SIZES: { - [key in CameraResolution]: { width: number; height: number }; +const CAMERA_RESOLUTION_DIMENSIONS: { + [key in CameraResolution]: PixelDimensions; } = { [CameraResolution.QNHD_180P]: { width: 320, height: 180 }, [CameraResolution.NHD_360P]: { width: 640, height: 360 }, @@ -68,8 +70,6 @@ export interface CameraConfig { /** * Specifies which camera to use if the devices has a front and a rear camera. If the device does not have a camera * meeting the requirements, the closest one will be used. - * - * @default `CameraFacingMode.ENVIRONMENT` */ facingMode: CameraFacingMode; /** @@ -78,12 +78,24 @@ export interface CameraConfig { * - The Monk Camera package will always try to fetch a stream with a 16:9 resolution format. * - The implementation of the algorithm used to choose the closest camera can differ between browsers, and if the * exact requirements can't be met, the resulting stream's quality can differ between browsers. - * - * @default `CameraResolution.UHD_4K` */ resolution: CameraResolution; } +/** + * Utility function that returns the dimensions in pixels of the given `CameraResolution`. + */ +export function getResolutionDimensions( + resolution: CameraResolution, + isPortrait = false, +): PixelDimensions { + const dimensions = CAMERA_RESOLUTION_DIMENSIONS[resolution]; + return { + width: isPortrait ? dimensions.height : dimensions.width, + height: isPortrait ? dimensions.width : dimensions.height, + }; +} + /** * This function is used by the Monk Camera package in order to add a layer of abstraction to the media constraints * passed to the `useUserMedia` hook. It takes an optional `CameraOptions` parameter and creates a @@ -94,7 +106,7 @@ export interface CameraConfig { * @see useUserMedia */ export function getMediaConstraints(config: CameraConfig): MediaStreamConstraints { - const { width, height } = CAMERA_RESOLUTION_SIZES[config.resolution]; + const { width, height } = getResolutionDimensions(config.resolution); const video: MediaTrackConstraints = { width: { ideal: width }, diff --git a/packages/public/camera-web/test/Camera/Camera.test.tsx b/packages/public/camera-web/test/Camera/Camera.test.tsx index 324c71d9b..0e0b4e1a0 100644 --- a/packages/public/camera-web/test/Camera/Camera.test.tsx +++ b/packages/public/camera-web/test/Camera/Camera.test.tsx @@ -44,55 +44,60 @@ describe('Camera component', () => { jest.clearAllMocks(); }); - it('should pass the resolution props to the useCameraPreview hook', () => { - const facingMode = CameraFacingMode.ENVIRONMENT; - const resolution = CameraResolution.HD_720P; - const { unmount } = render(); + it('should pass the proper props to the useCameraPreview hook', () => { + const { unmount } = render(); - expect(useCameraPreview).toHaveBeenCalledWith({ facingMode, resolution }); + expect(useCameraPreview).toHaveBeenCalledWith({ + facingMode: CameraFacingMode.ENVIRONMENT, + resolution: CameraResolution.UHD_4K, + }); unmount(); }); - it('should use CameraFacingMode.ENVIRONMENT as the default facingMode', () => { - const { unmount } = render(); - - expect(useCameraPreview).toHaveBeenCalledWith( - expect.objectContaining({ - facingMode: CameraFacingMode.ENVIRONMENT, - }), + it('should pass the proper props to the useCameraCanvas hook', () => { + const allowImageUpscaling = true; + const resolution = CameraResolution.QHD_2K; + const { unmount } = render( + , ); + + const streamDimensions = (useCameraPreview as jest.Mock).mock.results[0].value.dimensions; + + expect(useCameraCanvas).toHaveBeenCalledWith({ + allowImageUpscaling, + resolution, + streamDimensions, + }); unmount(); }); - it('should use CameraResolution.UHD_4K as the default resolution', () => { + it('should use the 4K resolution by default', () => { const { unmount } = render(); - expect(useCameraPreview).toHaveBeenCalledWith( - expect.objectContaining({ - resolution: CameraResolution.UHD_4K, - }), + expect(useCameraCanvas).toHaveBeenCalledWith( + expect.objectContaining({ resolution: CameraResolution.UHD_4K }), ); unmount(); }); - it('should pass the stream dimensions to the useCameraCanvas hook', () => { + it('should not use image upscaling by default', () => { const { unmount } = render(); - expect(useCameraCanvas).toHaveBeenCalledWith({ - dimensions: (useCameraPreview as jest.Mock)().dimensions, - }); + expect(useCameraCanvas).toHaveBeenCalledWith( + expect.objectContaining({ allowImageUpscaling: false }), + ); unmount(); }); - it('should pass the video ref, canvasRef and stream dimensions to the useCameraScreenshot hook', () => { + it('should pass the video ref, canvasRef and canvas dimensions to the useCameraScreenshot hook', () => { const { unmount } = render(); - const useCameraPreviewResultMock = (useCameraPreview as jest.Mock).mock.results[0].value; - const canvasRefMock = (useCameraCanvas as jest.Mock).mock.results[0].value.ref; + const videoRef = (useCameraPreview as jest.Mock).mock.results[0].value.ref; + const { ref: canvasRef, dimensions } = (useCameraCanvas as jest.Mock).mock.results[0].value; expect(useCameraScreenshot).toHaveBeenCalledWith({ - videoRef: useCameraPreviewResultMock.ref, - canvasRef: canvasRefMock, - dimensions: useCameraPreviewResultMock.dimensions, + videoRef, + canvasRef, + dimensions, }); unmount(); }); diff --git a/packages/public/camera-web/test/Camera/hooks/useCameraCanvas.test.ts b/packages/public/camera-web/test/Camera/hooks/useCameraCanvas.test.ts index 981fb1f7e..6bb2e52fc 100644 --- a/packages/public/camera-web/test/Camera/hooks/useCameraCanvas.test.ts +++ b/packages/public/camera-web/test/Camera/hooks/useCameraCanvas.test.ts @@ -1,6 +1,22 @@ +/* eslint-disable jest/valid-title */ +import { getResolutionDimensions } from '../../../src/Camera/hooks/utils'; + +jest.mock('../../../src/Camera/hooks/utils', () => ({ + ...jest.requireActual('../../../src/Camera/hooks/utils'), + getResolutionDimensions: jest.fn(() => ({ width: 1, height: 1 })), +})); + import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; -import { useCameraCanvas } from '../../../src/Camera/hooks'; +import { CameraCanvasConfig, CameraResolution, useCameraCanvas } from '../../../src/Camera/hooks'; + +function createProps(): CameraCanvasConfig { + return { + resolution: CameraResolution.HD_720P, + streamDimensions: { width: 4567, height: 1234 }, + allowImageUpscaling: false, + }; +} describe('useCameraCanvas hook', () => { afterEach(() => { @@ -10,22 +26,97 @@ describe('useCameraCanvas hook', () => { it('should return a ref object', () => { const useRefSpy = jest.spyOn(React, 'useRef'); - const { result, unmount } = renderHook(useCameraCanvas, { initialProps: { dimensions: null } }); + const { result, unmount } = renderHook(useCameraCanvas, { initialProps: createProps() }); expect(useRefSpy).toHaveBeenCalledWith(null); expect(result.current.ref).toBe(useRefSpy.mock.results[0].value); unmount(); }); - it('should update the dimensions of the canvas when the dimensions change', () => { - const ref = { current: { width: 0, height: 0 } }; - const dimensions = { width: 99, height: 112 }; + it('should not update the dimensions of the canvas when the stream dimensions are null', () => { + const width = 12; + const height = 24; + const ref = { current: { width, height } }; + const initialProps = { ...createProps(), streamDimensions: null }; jest.spyOn(React, 'useRef').mockImplementation(() => ref); - const { unmount } = renderHook(useCameraCanvas, { initialProps: { dimensions } }); + const { result, unmount } = renderHook(useCameraCanvas, { initialProps }); - expect(ref.current.width).toEqual(dimensions.width); - expect(ref.current.height).toEqual(dimensions.height); + expect(ref.current.width).toEqual(width); + expect(ref.current.height).toEqual(height); + expect(result.current.dimensions).toBeNull(); unmount(); }); + + [ + { + title: + 'should handle the case where asked resolution is bigger than stream resolution but upscaling is disabled', + streamDimensions: { width: 500, height: 500 }, + askedDimensions: { width: 5000, height: 8000 }, + allowImageUpscaling: false, + expected: { width: 500, height: 500 }, + }, + { + title: + 'should handle the case where asked resolution is bigger than stream resolution and upscaling is enabled', + streamDimensions: { width: 500, height: 500 }, + askedDimensions: { width: 5000, height: 5000 }, + allowImageUpscaling: true, + expected: { width: 5000, height: 5000 }, + }, + { + title: 'should handle the case where asked resolution is smaller than the stream resolution', + streamDimensions: { width: 5000, height: 5000 }, + askedDimensions: { width: 500, height: 500 }, + allowImageUpscaling: false, + expected: { width: 500, height: 500 }, + }, + { + title: 'should handle smaller resolution and wrong ratio with fit to width', + streamDimensions: { width: 5000, height: 5000 }, + askedDimensions: { width: 500, height: 800 }, + allowImageUpscaling: false, + expected: { width: 500, height: 500 }, + }, + { + title: 'should handle smaller resolution and wrong ratio with fit to height', + streamDimensions: { width: 5000, height: 5000 }, + askedDimensions: { width: 800, height: 400 }, + allowImageUpscaling: false, + expected: { width: 400, height: 400 }, + }, + { + title: 'should handle bigger resolution and wrong ratio with upscaling', + streamDimensions: { width: 500, height: 500 }, + askedDimensions: { width: 8000, height: 5000 }, + allowImageUpscaling: true, + expected: { width: 5000, height: 5000 }, + }, + ].forEach((params) => { + it(params.title, () => { + const ref = { current: { width: 0, height: 0 } }; + const initialProps = { + resolution: CameraResolution.QNHD_180P, + streamDimensions: params.streamDimensions, + allowImageUpscaling: params.allowImageUpscaling, + }; + jest.spyOn(React, 'useRef').mockImplementation(() => ref); + (getResolutionDimensions as jest.Mock).mockImplementationOnce(() => params.askedDimensions); + + const { result, unmount } = renderHook(useCameraCanvas, { initialProps }); + + expect(getResolutionDimensions).toHaveBeenCalledWith( + initialProps.resolution, + params.streamDimensions.width < params.streamDimensions.height, + ); + expect(ref.current.width).toEqual(params.expected.width); + expect(ref.current.height).toEqual(params.expected.height); + expect(result.current.dimensions).toEqual({ + width: params.expected.width, + height: params.expected.height, + }); + unmount(); + }); + }); }); diff --git a/packages/public/camera-web/test/Camera/hooks/utils/getMediaConstraints.test.ts b/packages/public/camera-web/test/Camera/hooks/utils/getMediaConstraints.test.ts index 79a11cee7..64ff06fa2 100644 --- a/packages/public/camera-web/test/Camera/hooks/utils/getMediaConstraints.test.ts +++ b/packages/public/camera-web/test/Camera/hooks/utils/getMediaConstraints.test.ts @@ -1,5 +1,5 @@ import { CameraFacingMode, CameraResolution } from '../../../../src'; -import { getMediaConstraints } from '../../../../src/Camera/hooks/utils'; +import { getMediaConstraints, getResolutionDimensions } from '../../../../src/Camera/hooks/utils'; const EXPECTED_FACING_MODE_VALUES: { [key in CameraFacingMode]: string; @@ -19,34 +19,57 @@ const EXPECTED_CAMERA_RESOLUTION_SIZES: { [CameraResolution.UHD_4K]: { width: 3840, height: 2160 }, }; -describe('useMediaConstraints hook', () => { - Object.values(CameraFacingMode).forEach((facingMode) => - it(`should properly map the '${facingMode}' facingMode option`, () => { - const constraints = getMediaConstraints({ facingMode, resolution: CameraResolution.UHD_4K }); - - expect(constraints).toEqual( - expect.objectContaining({ - video: expect.objectContaining({ facingMode: EXPECTED_FACING_MODE_VALUES[facingMode] }), - }), - ); - }), - ); - - Object.values(CameraResolution).forEach((resolution) => - it(`should properly map the '${resolution}' resolution option`, () => { - const constraints = getMediaConstraints({ - resolution, - facingMode: CameraFacingMode.ENVIRONMENT, - }); - - expect(constraints).toEqual( - expect.objectContaining({ - video: expect.objectContaining({ - width: { ideal: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].width }, - height: { ideal: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].height }, +describe('Media Constraints utils', () => { + describe('getResolutionDimensions function', () => { + Object.values(CameraResolution).forEach((resolution) => + it(`should return the proper dimensions for the '${resolution}' resolution`, () => { + const landscapeDimensions = getResolutionDimensions(resolution, false); + expect(landscapeDimensions).toEqual({ + width: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].width, + height: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].height, + }); + + const portraitDimensions = getResolutionDimensions(resolution, true); + expect(portraitDimensions).toEqual({ + width: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].height, + height: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].width, + }); + }), + ); + }); + + describe('useMediaConstraints hook', () => { + Object.values(CameraFacingMode).forEach((facingMode) => + it(`should properly map the '${facingMode}' facingMode option`, () => { + const constraints = getMediaConstraints({ + facingMode, + resolution: CameraResolution.UHD_4K, + }); + + expect(constraints).toEqual( + expect.objectContaining({ + video: expect.objectContaining({ facingMode: EXPECTED_FACING_MODE_VALUES[facingMode] }), + }), + ); + }), + ); + + Object.values(CameraResolution).forEach((resolution) => + it(`should properly map the '${resolution}' resolution option`, () => { + const constraints = getMediaConstraints({ + resolution, + facingMode: CameraFacingMode.ENVIRONMENT, + }); + + expect(constraints).toEqual( + expect.objectContaining({ + video: expect.objectContaining({ + width: { ideal: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].width }, + height: { ideal: EXPECTED_CAMERA_RESOLUTION_SIZES[resolution].height }, + }), }), - }), - ); - }), - ); + ); + }), + ); + }); }); diff --git a/packages/public/common/src/utils/array.utils.ts b/packages/public/common/src/utils/array.utils.ts index cf3eaf906..80afbf252 100644 --- a/packages/public/common/src/utils/array.utils.ts +++ b/packages/public/common/src/utils/array.utils.ts @@ -49,16 +49,6 @@ export function uniq(array: T[]): T[] { */ export type RecursiveArray = (T | RecursiveArray)[]; -function flattenRecursive(array: RecursiveArray, result: T[]): void { - array.forEach((item) => { - if (Array.isArray(item)) { - flattenRecursive(item, result); - } else { - result.push(item); - } - }); -} - /** * Flatten the given array. * @@ -67,9 +57,14 @@ function flattenRecursive(array: RecursiveArray, result: T[]): void { * // Output : 1,2,3,4,5,6 */ export function flatten(array: RecursiveArray): T[] { - const result: T[] = []; - flattenRecursive(array, result); - return result; + return array.reduce((acc, val) => { + if (Array.isArray(val)) { + acc.push(...flatten(val)); + } else { + acc.push(val); + } + return acc; + }, []); } /** diff --git a/packages/public/common/test/utils/promise.utils.test.ts b/packages/public/common/test/utils/promise.utils.test.ts index 918c72b91..ec18cc871 100644 --- a/packages/public/common/test/utils/promise.utils.test.ts +++ b/packages/public/common/test/utils/promise.utils.test.ts @@ -8,7 +8,7 @@ describe('Promise utils', () => { await timeoutPromise(delay); const actualDelay = Date.now() - startTime; expect(actualDelay).toBeGreaterThanOrEqual(delay); - expect(actualDelay).toBeLessThan(delay + 10); + expect(actualDelay).toBeLessThan(delay + 100); }); }); });