// https://github.com/purple-technology/react-camera-pro import React, { useState, useEffect, useRef, useImperativeHandle, useCallback, } from 'react'; import styled from '@emotion/styled'; type FacingMode = 'user' | 'environment'; type AspectRatio = 'cover' | number; // for example 16/9, 4/3, 1/1 type Stream = MediaStream | null; type SetStream = React.Dispatch>; type SetNumberOfCameras = React.Dispatch>; interface ErrorMessages { noCameraAccessible?: string; permissionDenied?: string; switchCamera?: string; canvas?: string; } interface CameraProps { facingMode?: FacingMode; aspectRatio?: AspectRatio; numberOfCamerasCallback?(numberOfCameras: number): void; videoSourceDeviceId?: string | undefined; errorMessages?: ErrorMessages; videoReadyCallback?(): void; flashMode?: boolean; onError?: (error?: string) => void; isTouchSupported?: boolean; touchSupportedCallback?(touchSupported: boolean): void; } export type CameraType = React.ForwardRefExoticComponent< CameraProps & React.RefAttributes > & { takePhoto(): string; switchCamera(): FacingMode; getNumberOfCameras(): number; flashStatus(): boolean; }; const Wrapper = styled.div` position: absolute; top: 0; left: 0; width: 100%; height: 100%; `; const Container = styled.div<{ aspectRatio: AspectRatio }>` width: 100%; ${({ aspectRatio }) => aspectRatio === 'cover' ? ` position: absolute; bottom: 0; top: 0; left: 0; right: 0;` : ` position: relative; padding-bottom: ${100 / aspectRatio}%;`} `; const CameraVideo = styled.video<{ mirrored: boolean }>` width: 100%; height: 100%; object-fit: cover; z-index: 0; transform: rotateY(${({ mirrored }) => (mirrored ? '180deg' : '0deg')}); `; const Canvas = styled.canvas` display: none; `; export const Camera = React.forwardRef( ( { facingMode = 'user', aspectRatio = 'cover', videoSourceDeviceId = undefined, errorMessages = { noCameraAccessible: 'No camera device accessible. Please connect your camera or try a different browser.', permissionDenied: 'Permission denied. Please refresh and give camera permission.', switchCamera: 'It is not possible to switch camera to different one because there is only one video device accessible.', canvas: 'Canvas is not supported.', }, videoReadyCallback = () => null, touchSupportedCallback = () => null, flashMode = false, isTouchSupported = false, onError = () => null, }, ref ) => { const player = useRef(null); const canvas = useRef(null); const container = useRef(null); const [numberOfCameras, setNumberOfCameras] = useState(0); const [stream, setStream] = useState(null); const [currentFacingMode, setFacingMode] = useState(facingMode); // const [torchsupported, setTorchSupported] = useState(false); // useEffect(() => { // touchSupportedCallback(torchsupported); // }, [torchsupported, touchSupportedCallback]); const toggleTorch = useCallback( (isOn: boolean): void => { const currentTrack = stream?.getVideoTracks()[0] || stream?.getTracks()[0]; if (isTouchSupported) { currentTrack?.applyConstraints({ advanced: [{ torch: isOn }], } as MediaTrackConstraintSet); } else { // console.log('Torch not supported'); } }, [isTouchSupported, stream] ); useEffect(() => { toggleTorch(flashMode); }, [flashMode, toggleTorch]); useImperativeHandle(ref, () => ({ takePhoto: () => { if (numberOfCameras < 1) { throw new Error(errorMessages.noCameraAccessible); } if (canvas?.current) { const playerWidth = player?.current?.videoWidth || 1280; const playerHeight = player?.current?.videoHeight || 720; const playerAR = playerWidth / playerHeight; const canvasWidth = container?.current?.offsetWidth || 1280; const canvasHeight = container?.current?.offsetHeight || 1280; const canvasAR = canvasWidth / canvasHeight; let sX, sY, sW, sH; if (playerAR > canvasAR) { sH = playerHeight; sW = playerHeight * canvasAR; sX = (playerWidth - sW) / 2; sY = 0; } else { sW = playerWidth; sH = playerWidth / canvasAR; sX = 0; sY = (playerHeight - sH) / 2; } canvas.current.width = sW; canvas.current.height = sH; const context = canvas.current.getContext('2d'); if (context && player?.current) { context.drawImage(player.current, sX, sY, sW, sH, 0, 0, sW, sH); } const imgData = canvas.current.toDataURL('image/jpeg'); return imgData; } else { onError(errorMessages.canvas); throw new Error(errorMessages.canvas); } }, switchCamera: () => { if (numberOfCameras < 1) { onError(errorMessages.noCameraAccessible); throw new Error(errorMessages.noCameraAccessible); } else if (numberOfCameras < 2) { onError( 'Error: Unable to switch camera. Only one device is accessible.' ); // console only } const newFacingMode = currentFacingMode === 'user' ? 'environment' : 'user'; setFacingMode(newFacingMode); return newFacingMode; }, getNumberOfCameras: () => { return numberOfCameras; }, flashStatus: () => () => { return isTouchSupported; }, })); useEffect(() => { initCameraStream( stream, setStream, currentFacingMode, videoSourceDeviceId, setNumberOfCameras, touchSupportedCallback, onError, errorMessages ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentFacingMode, videoSourceDeviceId]); useEffect(() => { if (stream && player && player.current) { player.current.srcObject = stream; } return () => { if (stream) { stream.getTracks().forEach((track) => { track.stop(); }); } }; }, [stream]); return ( {/* {notSupported ? ( {errorMessages.noCameraAccessible} ) : null} {permissionDenied ? ( {errorMessages.permissionDenied} ) : null} */} { videoReadyCallback(); }} /> ); } ); Camera.displayName = 'Camera'; const initCameraStream = ( stream: Stream, setStream: SetStream, currentFacingMode: FacingMode, videoSourceDeviceId: string | undefined, setNumberOfCameras: SetNumberOfCameras, setTorchSupported?: (touchSupported: boolean) => void, onError?: (error: string) => void, errorMessages?: ErrorMessages ) => { // stop any active streams in the window if (stream) { stream.getTracks().forEach((track) => { track.stop(); }); } const constraints = { audio: false, video: { deviceId: videoSourceDeviceId ? { exact: videoSourceDeviceId } : undefined, facingMode: currentFacingMode, width: { ideal: 1920 }, height: { ideal: 1920 }, }, }; if (navigator?.mediaDevices?.getUserMedia) { navigator.mediaDevices .getUserMedia(constraints) .then(async (stream) => { const result = await checkTorchExists(navigator?.mediaDevices, stream); setTorchSupported?.(result); setStream(handleSuccess(stream, setNumberOfCameras)); }) .catch((err) => { handleError(err, onError, errorMessages); }); } else { // const getWebcam = // navigator.getUserMedia || // navigator.webkitGetUserMedia || // navigator.mozGetUserMedia || // navigator.msGetUserMedia; // if (getWebcam) { // getWebcam( // constraints, // async (mediaStream: MediaStream) => { // const result = await checkTorchExists( // navigator?.mediaDevices, // mediaStream // ); // setTorchSupported(result); // setStream(handleSuccess(mediaStream, setNumberOfCameras)); // }, // (error: Error) => { // handleError(error, onError, errorMessages); // } // ); // } } }; const handleSuccess = ( stream: MediaStream, setNumberOfCameras: SetNumberOfCameras ) => { navigator.mediaDevices .enumerateDevices() .then((r) => setNumberOfCameras(r.filter((i) => i.kind === 'videoinput').length) ); return stream; }; const handleError = ( error: Error, onError?: (error: string) => void, errorMessages?: ErrorMessages ) => { console.error(error.name); //https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia if ( error.name === 'PermissionDeniedError' || error.name === 'NotAllowedError' ) { // setPermissionDenied(true); onError?.(errorMessages?.permissionDenied || 'Permission denied'); } else { // setNotSupported(true); onError?.( errorMessages?.noCameraAccessible || 'No camera device accessible' ); } }; async function checkTorchExists(device: any, stream: MediaStream) { const supportedConstraints = device?.getSupportedConstraints(); let track = stream.getTracks()[0]; try { if ( supportedConstraints && 'torch' in supportedConstraints && track && track.applyConstraints ) { try { await track.applyConstraints({ advanced: [{ torch: false }], } as MediaTrackConstraintSet); return true; } catch (error) { return false; } } else { return false; } } catch { return false; } }