Using Device Camera API and Toggling Between Cameras

In this post we'll learn how to use the native web browser camera API to do the following:

  1. Activate a camera feed.

  2. Capture what the camera see (taking a photo)

  3. Fetching all available devices and allowing the client to switch between cameras.

Demo of what we are going to build

As you can see, in the above example we have a webpage running on localhost server with nextjs technology. And a simple select & option HTML tags with a list of all currently connected cameras to the device (in that case macbook pro).

Our tech stack

  1. Nextjs

  2. Reactjs

  3. Typescript & javascript

  4. WEB camera API

  5. Basic HTML and CSS

Step 1 - setting up the environment from scratch

Lets run the create-next-app with npx:

npx create-next-app@latest --typescript

Notice we apply a typescript template.

Step 2 - setting up the index file

// src/pages/index.tsx

<main className={styles.main}>
        <div className={styles.description}>
          <h1 className={styles.description_title}>Web API Camera with React and Nextjs! &nbsp; </h1>
        </div>

        <div className={styles.description}>
            <p>In this app you will be able to interact with the camera api and select one of the few available cameras from given device.</p>
        </div>

        <div >
          <CameraFeed  sendFile={(base64Image: string) => console.log('file as base64:', base64Image)} />
        </div>
      </main>

Notice that we have deleted most of the nextjs boilerplate of the index file

Step 3 - streaming the camera feed into the HTML video tag

We’ll start by using react class components that return a basic div structure and HTML video tag element whose sole purpose is to start up the camera feature and show the feed from the camera into the page.

  1. Let's create a new file called camera-feed.tsx

  2. Let's create another new file called Camera.module.css

Our camera feed react class component will look like this:

interface CameraFeedPropsInterface {
  sendFile: any
}

interface CameraFeedStateInteraface {

}

export class CameraFeed extends Component<CameraFeedPropsInterface,CameraFeedStateInteraface> {
videoPlayer: any;

async componentDidMount() {
    this.initializeCamera();
}

initializeCamera() {
    this.setDevice();
}

async setDevice() {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });
    this.videoPlayer.srcObject = stream;

    setTimeout(() => {
        this.videoPlayer.play();
    }, 500)
}

render() {
    return (
    <div className={styles.camera_container}>
        <div>
            <video ref={ref => (this.videoPlayer = ref)} width="680" height="360" />
        </div>
    </div>
    );
}
}

There’s a bit more code than what we need here but we’ll slowly develop new features! Lets break down the code above.

Our typescript definition for our react class component. We’ll add some more types here in a moment.

interface CameraFeedPropsInterface {
    sendFile: any
}

interface CameraFeedStateInteraface {
    availableCameraDevices: CameraDeviceInputInfoInterface[]
    selectCamerasDeviceById: string
}

interface CameraDeviceInputInfoInterface {
    deviceId: string;
    groupId: string;
    kind: string;
    label: string;
}

We’ll also add here another type for the devices we’ll fetch from the api. Which is CameraDeviceInputInfoInterface that describe the given object of a device we get from the browser API.

async componentDidMount() {
        this.initializeCamera();
}

async setDevice() {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });
        this.videoPlayer.srcObject = stream;

        setTimeout(() => {
            this.videoPlayer.play();
        }, 500)
}

componentDidMount will help us to wait for the DOM to be ready so we can interact with the navigator and mediaDevices api.

setDevice is our main method to access the browser web api. Note that the camera api is async in nature since we are activating a function that is downloading a steam of pixels to the end user browser which means it can take a few seconds until the user see the stream of video.

Step 4 - How to capture photo with camera web API

To capture a photo of the above stream coming from the camera feed. We’ll need to:

  1. Create a button with a call to action.

  2. Create a place to print the photo (canvas).

  3. A function that reads the stream coming from the camera and captures a single frame.

Let's place the button and the canvas right after the video tag:

<button onClick={this.capturePhoto} className={styles.capture_photo}>Capture photo</button>
            <div className={styles.stage}>
                <canvas width="680" height="360" ref={ref => (this.canvas = ref)} />
            </div>

You’ll notice we are using className and styles base on nextjs css best practices. Next step we’ll do a little bit of css to style everything in place.

Capture Photo

Now we want to write the capture photo function that will grab a frame and push it into the canvas.

capturePhoto = () => {
    const { sendFile } = this.props;
    const context = this.canvas.getContext('2d');
    context.drawImage(this.videoPlayer, 0, 0, 680, 360);
    const dataBase64 = this.canvas.toDataURL("image/jpeg");
    sendFile(dataBase64);
}

This function sits inside our react class component and can be placed right after “setDevice” function.

Notice that we use canvas and toDataURL to convert the canvas into a base64 as jpeg format so we can pass it as data to our parent provided props function called “sendFile” - that function will help us in the future to do something with the captured photo. For example send it to a server API that will save the photo, or whatever we decide to do (not as part of this article).

Step 5 - let's write quick CSS

We’ve already created a css file called Camera.module.css that should be placed at src/styles folder of our nextjs project.

We are not going to dig into this code too much. Go learn some css. It's fun!

.list_of_avaialble_cameras {
  text-align: center;
  padding: 20px;
}

.capture_photo {
  font-size: 16px;
  padding: 5px;
  font-weight: 600;
  margin: 10px;
  cursor: pointer;
}

.stage {
  box-shadow: 0px 0px 3px #c7c4c4;
  border-radius: 1px;
}

Step 6 - accessing all available cameras per device

So each device, be it a laptop, an android phone or iphone have the ability to connect to more than one camera or device (video input or audio input etc). Some cameras are directly inserted into the device, some can be connected via a USB cable.

Here we’ll get the list of those camera devices and print them in a list so our client can use them as they wish and switch between them.

Print List of Cameras in JSX

Above the video tag, let’s add select and option tags that will iterate an array with all the available cameras that we’ll soon fetch from the web camera API.

<div className={styles.list_of_avaialble_cameras}>
                {<select onChange={(e) => this.pickCameraDevice(e.currentTarget.value)}>
                    <option disabled>Pick Camera</option>§
                   {this.state.avaialbleCamerasDevices.map((camera: CameraDeviceInputInfoInterface) => {
                    return <option key={camera.deviceId} value={camera.deviceId} selected={this.state.selectCamerasDeviceById === camera.deviceId}>{camera.label}</option>
                   })}
                </select>}
            </div>
  1. We use class name to apply list of available cameras styling

  2. We use select tag with onChange and a method that we’ll write soon that will pick a camera device from a possible list of camera

  3. We iterate the state where we’ll store all available camera devices with javascript’s map method.

  4. We’ll print the camera label as the name of the camera provided us by the web api.

Get a List of Cameras

In a new method to our camera feed class component we’ll create a async function that fetch a list of devices, filter them base on their “type”/”kind” (we want only video inputs):

async getListOfCameras() {
    const devices = await navigator.mediaDevices.enumerateDevices();
    const cameras = devices.filter((device) => device.kind === 'videoinput');
    return cameras;
}

Now we’ll want to extend our componentDidMount to populate the state with available cameras:

async componentDidMount() {
    const cameras = await this.getListOfCameras();
    this.setState({
        avaialbleCamerasDevices: cameras
    });

    this.initializeCamera(cameras);
}

We also want to create a constructor with initialized empty state so the above setState will work:

constructor(props: CameraFeedPropsInterface) {
    super(props);
    this.state = {
        avaialbleCamerasDevices: [],
        selectCamerasDeviceById: ''
    };
}

Picking a camera

A new method was introduced to our react class component that will go over current camera devices and pick one based on the selected one from the list of cameras. Note that we do attach with the onChange event (above in the JSX).

pickCameraDevice = (deviceId: string) => {
    const device: CameraDeviceInputInfoInterface = this.state.avaialbleCamerasDevices.find((device: CameraDeviceInputInfoInterface) => device.deviceId === deviceId) as CameraDeviceInputInfoInterface;
    this.setDevice(device);
}

Done - summary

Good job! We have a running webpage with access to the camera feed, a button that allows us to capture a photo and a list of all possible cameras in the current running device!

If you have problems, or errors you can look at the github source code here or write them down in the comment section below and i’ll try to help.