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
getUserMediain secure contexts, that is, from pages served viahttps://or fromlocalhost. If you open your file directly with thefile://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.
- Modern browsers only allow using
- 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.
- When you call
- Always use the modern API
- Today everything is done with
navigator.mediaDevices.getUserMedia().
Versions with vendor prefixes likenavigator.webkitGetUserMedia,navigator.mozGetUserMedia, ornavigator.msGetUserMediaare officially deprecated and removed from modern browsers. Do not use them in new code.
- Today everything is done with
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 + microphoneYou 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 aTypeError.successCallback(legacy API): function that executes if the user approves the permission and the stream loads successfully. Receives aLocalMediaStreamobject 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:
| Error | Description |
|---|---|
PERMISSION_DENIED / NotAllowedError | The user denied permission to access the requested multimedia device. |
NOT_SUPPORTED_ERROR / NotSupportedError | A specified constraint is not supported by the browser or device. |
MANDATORY_UNSATISFIED_ERROR | No multimedia sources of the type specified in the constraints were found. |
NO_DEVICES_FOUND / NotFoundError | No 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
getUserMediaon pages whose URL starts withfile://. You always need a local server orhttps://. - 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(formerlyPERMISSION_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(formerlyNOT_SUPPORTED_ERROR)- The requested
constraintis not supported on that device or browser. - Classic example: requesting
video: trueon a desktop computer without a camera.
- The requested
-
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. Uselocalhostor a server withhttps://. - 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—specificallynavigator.permissions.query({ name: "camera" })— to know if the user already granted or denied access before callinggetUserMedia. - If you need to record video, add the
MediaRecorder APIto your workflow. It is the natural next step once you mastergetUserMedia. - 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
- Why is my camera not working if I open the HTML file directly?
- Because
file://is not a secure context (secure context). You must use a local server likelocalhostor publish your app underhttps://.
- Because
- Can I capture photos without plugins or external libraries?
- Yes. You only need a
<video>element for the stream and a<canvas>element to capture the frame. Everything with native JavaScript.
- Yes. You only need a
- How do I also capture the microphone at the same time?
- Include
{ audio: true }in yourconstraintsobject alongsidevideo: true. The browser will request permission for both devices in a single dialog.
- Include
- Is it mandatory to use
srcObject?- Yes, it is the modern and recommended method for assigning a
MediaStreamto a<video>element. The use ofURL.createObjectURL(stream)is deprecated and removed in current browsers.
- Yes, it is the modern and recommended method for assigning a
- What happens if the user denies camera permissions?
- The
Promisewill be rejected with aNotAllowedErrorerror. 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.
- The
- Does
getUserMediawork in Safari?- Yes. Safari supports
navigator.mediaDevices.getUserMediastarting from Safari 11 on macOS and iOS 11 on iPhone/iPad. Remember to add theplaysinlineattribute to the<video>element so it plays properly on iOS without going fullscreen.
- Yes. Safari supports
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