Build a React Camera Component with Hooks
With the getUserMedia
API, we can access the camera in web browsers, which makes it possible to do computer vision tasks like barcode scanning. In this article, we are going to build a camera component for React with hooks.
Build a React Camera Component with Hooks
We are going to write a React camera component with hooks using Typescript.
New project
-
Init a new npm project.
npm init
-
Install dev dependencies.
npm install --save-dev typescript @types/react react react-dom
Since we are going to publish it as a library, we install the packages as
devDependencies
.
Create the entry file and the component
-
Create a component named
src/VisionCamera.tsx
with the basic content:import React from 'react'; const VisionCamera = (): React.ReactElement => { return ( <div> Vision Camera </div> ) } export default VisionCamera;
-
Create an entry file named
src/index.js
with the following content:import VisionCamera from './VisionCamera' export * from "./VisionCamera" export { VisionCamera };
TypeScript configuration
-
Create a
tsconfig.json
file with the following content:{ "compilerOptions": { "target": "es2015", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "baseUrl": ".", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react", "module": "commonjs", "moduleResolution": "node", "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "declaration": true, "strictNullChecks": false, "outDir": "dist", "rootDir": "src" }, "exclude": [ "node_modules", "cypress" ], "include": [ "src/**/*" ] }
-
Update the
package.json
file to add a build script."scripts": { "build": "tsc" }
Now, we can run npm run build
to build the library.
Create an example project to test the component
We can create a new project to test the component.
-
New project with Create React App in the component project’s folder.
npx create-react-app example --template typescript
-
Install the component.
npm install ../
-
Use
npm link
to use the React in the component project’snode_modules
.npm link ../node_modules/react npm link ../node_modules/react-dom
-
Update
App.tsx
to use the camera component.import React from 'react'; import {VisionCamera} from 'react-vision-camera'; function App() { return ( <div> <VisionCamera></VisionCamera> </div> ); } export default App;
-
We can now run the example project to test the component.
npm run start
Implement the camera component
Define props for the component
export interface CameraProps{
isActive?:boolean; // whether the camera is opened
isPause?:boolean; // whether the camera is paused
desiredCamera?:string;
desiredResolution?:Resolution;
facingMode?:string;
children?: ReactNode;
onOpened?: (cam:HTMLVideoElement,camLabel:string) => void; // event triggered when the camera is opened
onClosed?: () => void; // event triggered when the camera is closed
onDeviceListLoaded?: (list:MediaDeviceInfo[]) => void; // event triggered when the list of camera devices is loaded
}
const VisionCamera = (props:CameraProps): React.ReactElement => {
}
Add a video element
-
Update the JSX with the following:
return ( <div style={{ position:"relative", width:"100%", height:"100%", left:0, top:0 }}> <video style={{ position:"absolute", objectFit:"cover", width:"100%", height:"100%", left:0, top:0 }} ref={camera} muted autoPlay={true} playsInline={true} onLoadedData={onCameraOpened}></video> {props.children} </div> )
Because we do not use a bundler like webpack and rollup, here we use inline styles instead of a CSS file.
-
Add
ref
for the video element and the handler for theonLoadedData
event.const camera = React.useRef(null); const onCameraOpened = () => { console.log("onCameraOpened"); if (props.onOpened) { props.onOpened(camera.current,getCurrentCameraLabel()); } }
Add camera control functions
-
Load camera devices into a list.
const loadDevices = async () => { const constraints = {video: true, audio: false}; const stream = await navigator.mediaDevices.getUserMedia(constraints) // ask for permission const mediaDevices = await navigator.mediaDevices.enumerateDevices(); let cameraDevices = []; for (let i=0;i<mediaDevices.length;i++){ let device = mediaDevices[i]; if (device.kind == 'videoinput'){ // filter out audio devices cameraDevices.push(device); } } devices.current = cameraDevices; const tracks = stream.getTracks(); for (let i=0;i<tracks.length;i++) { const track = tracks[i]; track.stop(); // stop the opened camera } if (props.onDeviceListLoaded) { props.onDeviceListLoaded(cameraDevices); } }
-
Stop the opened camera.
const stop = () => { try{ if (localStream.current){ const stream = localStream.current as MediaStream; const tracks = stream.getTracks(); for (let index = 0; index < tracks.length; index++) { const track = tracks[index]; track.stop(); } if (props.onClosed) { props.onClosed(); } } } catch (e){ console.log(e); } };
-
Start the camera with desired conditions.
const play = (options:PlayOptions) => { stop(); // close before play var constraints:any = {}; if (options.deviceId){ constraints = { video: {deviceId: options.deviceId}, audio: false } }else{ constraints = { video: {width:1280, height:720}, audio: false } } if (options.facingMode) { delete constraints["video"]["deviceId"]; constraints["video"]["facingMode"] = { exact: options.facingMode }; } if (options.desiredResolution) { constraints["video"]["width"] = options.desiredResolution.width; constraints["video"]["height"] = options.desiredResolution.height; } navigator.mediaDevices.getUserMedia(constraints).then(function(stream) { localStream.current = stream; // Attach local stream to video element camera.current.srcObject = stream; }).catch(function(err) { if (options.facingMode) { // facing mode not supported on desktop Chrome delete options["facingMode"]; play(options); }else{ console.error('getUserMediaError', err, err.stack); } }); } interface PlayOptions { deviceId?:string; desiredResolution?:Resolution; facingMode?:string; }
-
Helper functions:
const playWithDesired = async () => { if (!devices.current) { await loadDevices(); // load the camera devices list if it hasn't been loaded } let desiredDevice = getDesiredDevice(devices.current) if (desiredDevice) { let options:PlayOptions = {}; options.deviceId=desiredDevice; if (props.desiredResolution) { options.desiredResolution=props.desiredResolution; } if (props.facingMode) { options.facingMode=props.facingMode; } play(options); }else{ throw new Error("No camera detected"); } } const getDesiredDevice = (devices:MediaDeviceInfo[]) => { var count = 0; var desiredIndex = 0; for (var i=0;i<devices.length;i++){ var device = devices[i]; var label = device.label || `Camera ${count++}`; if (props.desiredCamera) { if (label.toLowerCase().indexOf(props.desiredCamera.toLowerCase()) != -1) { desiredIndex = i; break; } } } if (devices.length>0) { return devices[desiredIndex].deviceId; // return the device id }else{ return null; } }
Add hooks to use the camera functions
-
When the component is mounted, load the camera devices list and start the camera if the
isActive
prop istrue
.const init = async () => { if (!devices.current) { await loadDevices(); // load the camera devices list when the component is mounted } if (props.isActive === true) { playWithDesired(); } mounted.current = true; } init();
-
Monitor the change of the
isActive
prop. If it is set to true, start the camera, otherwise, stop the camera.React.useEffect(() => { if (mounted.current === true) { if (props.isActive === true) { playWithDesired(); }else{ stop(); } } }, [props.isActive]);
-
Monitor the change of the
isPause
prop. If it is set to true, pause the video, otherwise, resume the video.React.useEffect(() => { if (mounted.current === true) { if (camera.current && props.isActive === true) { if (props.isPause === true) { camera.current.pause(); }else{ camera.current.play(); } } } }, [props.isPause]);
-
Monitor the changes of the desired camera configurations. If there are changes, restart the camera.
React.useEffect(() => { if (props.isActive === true && localStream.current && mounted.current === true) { playWithDesired(); } }, [props.desiredCamera,props.desiredResolution,props.facingMode]);
Update the example to use the camera component
Now, we can use the component in the example project to open the camera.
<div className="vision-camera">
<VisionCamera
isActive={true}
desiredCamera="back"
desiredResolution={{width:1280,height:720}}
>
</VisionCamera>
</div>
You can check out the online demo to have a try.
In the next article, we are going to build a barcode scanner component based on this camera component.