JavaScript Camera API: Access the camera and microphone using getUserMedia

- Andrés Cruz - ES En español

JavaScript Camera API: Access the camera and microphone using getUserMedia

When I first discovered that HTML5 allowed opening the camera without plugins, I thought exact identical to what you are surely thinking now: "this is fascinating". Being able to access user video and audio directly from JavaScript radically changed the way we build web applications: from QR code readers to video calls or image capture systems, all of this brings web apps closer to the experience level that previously only native apps offered.

In this guide I will teach you, step by step, how to access the camera and microphone with the JavaScript Camera API, how to display the stream in a <video> element, how to capture a photo with <canvas>, and how to resolve the typical errors that we have all encountered at some point.

This is one of the most powerful features brought by the HTML5 Canvas API: accessing the user's camera and microphone (with their prior approval) from any computer or mobile device, without needing to install any plugin. In this article we will learn how to obtain that data stream (stream) and direct it inside a <video> element to, finally, capture images through the user's camera.

Requirements, permissions, and limitations: HTTPS, compatibility, and common errors

Before writing a single line of code, there are three fundamental rules you must know to avoid frustration:

  • You need HTTPS (or localhost)
    • Modern browsers only allow using getUserMedia in secure contexts, that is, from pages served via https:// or from localhost. If you open your file directly with the file:// protocol, the API simply will not be available. The first time I tried it from a standalone file, I went crazy: the code was correct, but the camera didn't show up anywhere. This secure context restriction is mandatory according to the W3C standard and is respected by Chrome, Firefox, Safari, and Edge.
  • The user must always approve the permission
    • When you call getUserMedia(), the browser automatically displays a popup requesting access to the camera and/or microphone. The user can accept or deny. If they deny —or if the browser remembers a previous denial— an error will be triggered that you must catch and handle in a user-friendly way.
  • Always use the modern API
    • Today everything is done with navigator.mediaDevices.getUserMedia().
      Versions with vendor prefixes like navigator.webkitGetUserMedia, navigator.mozGetUserMedia, or navigator.msGetUserMedia are officially deprecated and removed from modern browsers. Do not use them in new code.

How to access the camera with JavaScript: the JavaScript Camera API

The foundation of everything is navigator.mediaDevices.getUserMedia(). This method returns a Promise and receives an object called constraints specifying what you want to activate: video, audio, or both. If the user approves the permission, the promise resolves with a MediaStream object ready to use.

The constraints object of getUserMedia

Typical configurations are as follows. You must specify at least one as true; otherwise, the call will throw a TypeError:

{ video: true, audio: false } // Camera only (webcam)
{ video: false, audio: true } // Microphone only
{ video: true, audio: true }  // Camera + microphone

You can also pass advanced configurations inside video to control resolution, front/rear camera on mobile devices, or frames per second:

{ video: { width: 1280, height: 720, facingMode: "user" }, audio: false }

The facingMode: "user" property activates the front camera on mobile devices, while facingMode: "environment" activates the rear camera. Very useful if you are building a mobile app.

Below are the three key parameters of the getUserMedia call:

  • constraints: configuration object indicating which data stream to activate. Possible cases:
    • { video: true, audio: true }: Enables both; the stream will contain video and audio.
    • { video: true, audio: false }: Video only.
    • { video: false, audio: true }: Audio/microphone only.
    • { video: false, audio: false }: This combination is NOT valid and the browser will throw a TypeError.
      • successCallback (legacy API): function that executes if the user approves the permission and the stream loads successfully. Receives a LocalMediaStream object with the data stream ready to assign to the <video> element.
      • errorCallback (optional, legacy API): function that executes if the user denies access or if any other failure occurs while loading the stream. Receives one of the following error codes:
ErrorDescription
PERMISSION_DENIED / NotAllowedErrorThe user denied permission to access the requested multimedia device.
NOT_SUPPORTED_ERROR / NotSupportedErrorA specified constraint is not supported by the browser or device.
MANDATORY_UNSATISFIED_ERRORNo multimedia sources of the type specified in the constraints were found.
NO_DEVICES_FOUND / NotFoundErrorNo webcam or microphone available on the system was found.

Complete reference available in the official documentation of MDN: MediaDevices.getUserMedia().

Complete example: accessing the camera and microphone with JavaScript

Below you will see the complete example. The first version uses the legacy API with compatibility prefixes (now deprecated) and the second is the modern, recommended approach using a Promise:

Legacy version (historical reference only, do not use in production):

// Compatibility with older browsers (deprecated)
navigator.getUserMedia = ( navigator.getUserMedia ||
                       navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia ||
                       navigator.msGetUserMedia);

navigator.getUserMedia(

   // constraints
   {
      video: true,
      audio: false
   },

   // successCallback
   function(localMediaStream) {
      var video = document.querySelector("video");
      video.src = window.URL.createObjectURL(localMediaStream);
   },

   // errorCallback
   function(err) {
    console.log("The following error occurred: " + err);
   }

);

In this example, we first assign the constraints indicating that we are only interested in video:

// constraints
{
   video: true,
   audio: false
},

If no error occurred, we assign the returned data stream in the LocalMediaStream object directly to the <video> element:

// successCallback
function(localMediaStream) {
   var video = document.querySelector("video");
   video.src = window.URL.createObjectURL(localMediaStream);
},

And finally, we handle any error that may occur:

// errorCallback
function(err) {
 console.log("The following error occurred: " + err);
}

Two important limitations to remember:

  • You cannot use getUserMedia on pages whose URL starts with file://. You always need a local server or https://.
  • The API requests explicit permission from the user before accessing the camera or microphone. You never get silent access.

Modern version with Promises (recommended):

navigator.mediaDevices.getUserMedia({ video: true })
  .then(stream => {
    const video = document.querySelector("video");
    video.srcObject = stream;
    video.play();
  })
  .catch(error => {
    console.error("An error occurred while accessing the camera:", error.name, error.message);
  });

Note the use of video.srcObject instead of the deprecated window.URL.createObjectURL(). This is the current standard recommended by MDN and W3C.

▶️ Displaying video in a <video> element

Your HTML can be as simple as this:

<video autoplay playsinline></video>

The autoplay attribute makes the video start as soon as the stream is available, and playsinline is essential on iOS to prevent Safari from opening it in fullscreen. As soon as the user approves permission, the video will appear like magic, just like in a native app.

️ How to also access the microphone with JavaScript

Accessing the microphone is exactly the same as the camera: only the constraint you activate changes. This is especially useful if you are building voice recorders, transcribers, or audio recognition systems in the browser:

navigator.mediaDevices.getUserMedia({ audio: true })
 .then(stream => {
   console.log("Microphone ready:", stream);
 })
 .catch(error => {
   console.error("Error accessing the microphone:", error.name);
 });

️ Camera and microphone in the same stream

If you need to capture audio and video simultaneously —for example, for a video call or a recording— simply combine both constraints in a single call. The browser will request permission for both devices at once:

navigator.mediaDevices.getUserMedia({
 video: true,
 audio: true
})
.then(stream => {
 const video = document.querySelector("video");
 video.srcObject = stream;
 video.play();
})
.catch(error => {
 console.error("Error accessing the camera or microphone:", error.name);
});

Taking photos from the webcam with <canvas>

This is one of my favorite uses for this API.
To capture a static frame from the <video> element in real time, all you need is a <canvas> element and the drawImage() method. The result is instant: an exact snapshot of what the camera is showing at that moment.

️ Linking <canvas> to the video stream

First, define the basic HTML structure:

<video autoplay playsinline></video>
<canvas id="canvas" width="640" height="480"></canvas>
<button id="capture">Take photo</button>

Then, in JavaScript, listen for the button click and draw the current video frame onto the canvas using drawImage():

const video = document.querySelector("video");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

document.getElementById("capture").addEventListener("click", () => {
 ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
});

When I implemented this in a real project, seeing the frozen image on the canvas was like having Photoshop integrated directly into the browser. The drawImage() method takes the <video> element as a source and "photographs" it at that exact instant.

Saving or downloading the captured image

Once you have the image on the canvas, you can convert it to base64 with toDataURL() and treat it like any image: display it, upload it to a server, or force its download:

const dataURL = canvas.toDataURL("image/png");

// Display in an <img> element
document.getElementById("preview").src = dataURL;

// Force download
const link = document.createElement("a");
link.download = "capture.png";
link.href = dataURL;
link.click();

❌ Error handling and denied permissions in getUserMedia

This is where the most common errors cluster. In the modern API, all errors land in the .catch() block as a DOMException object with a name property that identifies the failure type:

  • NotAllowedError (formerly PERMISSION_DENIED)
    • Occurs when the user rejects access or when the browser has already blocked camera permissions for that site. This usually happens to people who accidentally click "Block" the first time. Solution: inform the user how to reset permissions from browser settings.
  • NotSupportedError (formerly NOT_SUPPORTED_ERROR)
    • The requested constraint is not supported on that device or browser.
    • Classic example: requesting video: true on a desktop computer without a camera.
  • NotFoundError / NO_DEVICES_FOUND
    • No camera or microphone available was found on the system. It also happens when the device is currently in use by another application.

A robust error handler would look like this:

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    document.querySelector("video").srcObject = stream;
  })
  .catch(error => {
    switch (error.name) {
      case "NotAllowedError":
        alert("Permission denied. Please authorize camera access in your browser settings.");
        break;
      case "NotFoundError":
        alert("No camera was found on this device.");
        break;
      case "NotSupportedError":
        alert("Your browser or device does not support this feature.");
        break;
      default:
        console.error("Unexpected error:", error.name, error.message);
    }
  });

Practical tips after implementing it in real projects

  • Never test from file://: the API simply will not work. Use localhost or a server with https://.
  • Avoid prefixed APIs (webkitGetUserMedia, mozGetUserMedia): they are deprecated and removed in modern browsers.
  • Always catch errors, especially on mobile where permission edge cases are more varied.
  • Check existing permissions: use the Permissions API —specifically navigator.permissions.query({ name: "camera" })— to know if the user already granted or denied access before calling getUserMedia.
  • If you need to record video, add the MediaRecorder API to your workflow. It is the natural next step once you master getUserMedia.
  • Always stop the stream when it is no longer needed to release the camera and the indicator LED: stream.getTracks().forEach(track => track.stop()).

❓ Frequently Asked Questions about the JavaScript Camera API

  1. Why is my camera not working if I open the HTML file directly?
    1. Because file:// is not a secure context (secure context). You must use a local server like localhost or publish your app under https://.
  2. Can I capture photos without plugins or external libraries?
    1. Yes. You only need a <video> element for the stream and a <canvas> element to capture the frame. Everything with native JavaScript.
  3. How do I also capture the microphone at the same time?
    1. Include { audio: true } in your constraints object alongside video: true. The browser will request permission for both devices in a single dialog.
  4. Is it mandatory to use srcObject?
    1. Yes, it is the modern and recommended method for assigning a MediaStream to a <video> element. The use of URL.createObjectURL(stream) is deprecated and removed in current browsers.
  5. What happens if the user denies camera permissions?
    1. The Promise will be rejected with a NotAllowedError error. You must catch it in the .catch() block and display a clear message explaining how the user can reset the permission in their browser settings.
  6. Does getUserMedia work in Safari?
    1. Yes. Safari supports navigator.mediaDevices.getUserMedia starting from Safari 11 on macOS and iOS 11 on iPhone/iPad. Remember to add the playsinline attribute to the <video> element so it plays properly on iOS without going fullscreen.

Conclusion

Accessing the camera and microphone with JavaScript is simpler than it seems. With the modern MediaDevices.getUserMedia API, you can build everything from simple photo capture tools to full recording, real-time streaming, and video calling applications—all without relying on any external plugins.

The key is mastering the constraints object, properly assigning the stream with srcObject, handling real errors returned by the API —especially NotAllowedError and NotFoundError— and understanding how secure context permissions work in modern browsers. From here, you have everything you need to build any browser-based multimedia tool.

Recommended next experiment: How to create circle rings in JavaScript and Canvas

Learn how to use navigator.mediaDevices.getUserMedia in JavaScript to access the camera and microphone, capture photos with canvas, and handle permission errors.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.