WebRTC — Switch Cameras using Javascript getUserMedia

To fetch or toggle between the front-facing camera and the back camera, we can simply use the "facingMode" which is widely supported in the majority of the browsers.

WebRTC — Switch Cameras using Javascript getUserMedia

What is Facing Mode?

We can provide a lot of different audio and video constrains to capture the desired media from the device. 'facingMode' is one of the video constraints responsible for fetching a video stream from different cameras.

As per the documentation, the permitted values for the facing mode are:

"user":
The video source is facing toward the user; this includes, for example, the front-facing camera on a smartphone.

"environment":
The video source is facing away from the user, thereby viewing their environment. This is the back camera on a smartphone.

"left":
The video source is facing toward the user but to their left, such as a camera aimed toward the user but over their left shoulder.

"right":
The video source is facing toward the user but to their right, such as a camera aimed toward the user but over their right shoulder.

Why Facing Mode?

You may ask that what is the need of the facing mode if we can get the list of all the devices (enumerateDevices()) and select a specific device rather than relying on the browser to provide us the camera?

Possible answers could be:

  • This option is straight forward and easy to use.
  • Facing mode is widely supported in the majority of the browsers.
  • No worries about what camera to select (if you have multiple cameras on both the sides).

Even though today we have multiple back and front cameras, honestly, I've never seen any application which requires a specific front or a back camera. If your application has such requirements, you need to look for alternate methods (eg. enumerateDevices()).

Let's jump to the code

Now we have the basic understanding of the facing mode to start writing the code.

Check whether facing mode is supported by the browser!

Quite obvious, isn't it? I'm sure you don't want to run into some weird error and spend hours finding the solution and end up realizing this constraint is not supported in a very old browser.

const supports = navigator.mediaDevices.getSupportedConstraints();
if (!supports['facingMode']) {
    alert('This browser does not support facingMode!');
}

Adding the facing mode constraint

While specifying the getUserMedia options, you need to add this video constraint like this:

const options = {
    audio: false,
    video: {
        facingMode: 'user', // Or 'environment'
    },
};

const stream = await navigator.mediaDevices.getUserMedia(options);

You may ask this question:

Why are we not using exact keyword?
eg. facingMode: { exact: 'user' }?

If you use exact it usually throws OverconstrainedError error on the desktops as they don't always have multiple cameras.

But how to switch between the cameras?

If you simply put the code mentioned above inside a function and try to toggle between user and environment, it's highly likely that you'll see this error:

NotReadableError: Could not start video source

Why this error?

This error usually occurs when either your camera is being used by some other application or you try to stream from a different camera before closing the existing camera stream.

Let's fix this error by stopping the existing stream

// Stop the tracks
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());

// Provide new options
stream = await navigator.mediaDevices.getUserMedia(options);

// Add this stream to the video object
videoElm.srcObject = null;
videoElm.srcObject = stream;
videoElm.play();

You can also optimize this by adding a filter to stop only the video stream. Each track contains their own kind (video or audio) and you can access this using track.kind inside the loop.

How to get the deviceId?

I've seen most of the people use some services like Twilio. Those libraries mostly require us to provide the deviceId. It is quite simple to fetch the deviceId from the track.

const tracks = stream.getTracks();

tracks.forEach(track => {
  console.log(track.getSettings().deviceId);
});

It's demo time!

Let's put everything together and see the things working together.

The demo will throw NotAllowedError: Permission denied error because it is trying to access camera from some other domain. You can open this on CodePen.io and try.


Footnotes

If you have any doubts or you're facing any problems, you can follow or ping me on Twitter. I would love to know your experience or any other feedback.