Use navigator.mediaDevices.getUserMedia() to request camera access, attach its stream to a <video> element, and draw the current video frame to a canvas. The canvas method is the clearest option for a still photo you want to preview, crop, edit, or export. If your code already works with a MediaStreamTrack, ImageCapture.grabFrame() is an alternative that returns an ImageBitmap.
What you need before capturing a frame
- Serve the page from a secure context: HTTPS or localhost. In an insecure context,
navigator.mediaDevicesmay be unavailable. - Use a browser and device with a camera, and be ready to grant camera permission.
- Provide a video element, a capture button, and a canvas (which can be hidden if you only need the exported image).
The modern API is navigator.mediaDevices.getUserMedia(). The older callback-style navigator.getUserMedia() is deprecated; use the Promise-based MediaDevices method instead (MDN: deprecated Navigator.getUserMedia()).
Capture a frame with video and canvas
This complete example requests the camera after the user presses Start, waits for video dimensions before enabling capture, draws a frame when Capture is pressed, and provides a PNG preview. It also stops the camera when the user is finished.
<video id="camera" autoplay playsinline></video>
<button id="start" type="button">Start camera</button>
<button id="capture" type="button" disabled>Capture frame</button>
<button id="stop" type="button" disabled>Stop camera</button>
<p id="status" role="status">Camera is off.</p>
<canvas id="snapshot" hidden></canvas>
<img id="preview" alt="Captured camera frame">
<script>
const video = document.querySelector("#camera");
const canvas = document.querySelector("#snapshot");
const startButton = document.querySelector("#start");
const captureButton = document.querySelector("#capture");
const stopButton = document.querySelector("#stop");
const status = document.querySelector("#status");
const preview = document.querySelector("#preview");
let stream;
function stopCamera() {
if (stream) {
stream.getTracks().forEach(track => track.stop());
stream = undefined;
}
video.srcObject = null;
captureButton.disabled = true;
stopButton.disabled = true;
startButton.disabled = false;
status.textContent = "Camera is off.";
}
startButton.addEventListener("click", async () => {
startButton.disabled = true;
status.textContent = "Waiting for camera permission…";
try {
stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await video.play();
if (video.videoWidth && video.videoHeight) {
captureButton.disabled = false;
stopButton.disabled = false;
status.textContent = "Camera is ready.";
} else {
video.addEventListener("loadedmetadata", () => {
if (video.videoWidth && video.videoHeight) {
captureButton.disabled = false;
stopButton.disabled = false;
status.textContent = "Camera is ready.";
}
}, { once: true });
}
} catch (error) {
startButton.disabled = false;
status.textContent = `Could not start camera: ${error.name}. Check permission, device, and page security.`;
console.error(error);
}
});
captureButton.addEventListener("click", () => {
const width = video.videoWidth;
const height = video.videoHeight;
if (!width || !height) {
status.textContent = "No video frame is ready yet.";
return;
}
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
status.textContent = "A 2D canvas context is unavailable.";
return;
}
context.drawImage(video, 0, 0, width, height);
preview.src = canvas.toDataURL("image/png");
status.textContent = `Captured a ${width} × ${height} PNG frame.`;
});
stopButton.addEventListener("click", stopCamera);
window.addEventListener("pagehide", stopCamera);
</script>
getUserMedia() prompts for permission and resolves to a MediaStream (MDN: MediaDevices.getUserMedia()). The video element displays that stream; when its videoWidth and videoHeight are nonzero, a frame can be drawn.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Choose the capture dimensions
The example uses the dimensions reported by the video, so it captures the displayed camera frame at the stream’s intrinsic size rather than stretching it to arbitrary dimensions. Set the canvas dimensions before calling drawImage(); otherwise, the browser’s default canvas size may produce an unexpectedly small result. If you intentionally want a smaller output, choose a target width and height that preserve the source aspect ratio, then draw into those dimensions.
Export a file or upload the image
canvas.toDataURL("image/png") returns a data URL, which is convenient for a quick preview. For larger images or uploads, prefer the asynchronous canvas.toBlob() method to avoid keeping a large base64 string in memory:
canvas.toBlob(async blob => {
if (!blob) throw new Error("Canvas image could not be created");
const form = new FormData();
form.append("photo", blob, "camera-frame.png");
await fetch("/upload", { method: "POST", body: form });
}, "image/png");
Replace /upload with an endpoint on your application that accepts multipart form data. Do not treat a client-side capture as trusted evidence or skip the server-side checks your application needs.
Crop or process the frame
drawImage() supports source and destination rectangles, making canvas useful when you need a crop or a transformed output. For example, to draw the central square from a landscape video, calculate the crop from the intrinsic dimensions and pass the source rectangle:
Recommended Free Tools
Rank #2
const side = Math.min(video.videoWidth, video.videoHeight);
const sx = (video.videoWidth - side) / 2;
const sy = (video.videoHeight - side) / 2;
canvas.width = side;
canvas.height = side;
canvas.getContext("2d").drawImage(
video, sx, sy, side, side, 0, 0, side, side
);
Use ImageCapture.grabFrame() when working with a track
ImageCapture.grabFrame() takes a snapshot of live video from a MediaStreamTrack and returns an ImageBitmap when successful (MDN: ImageCapture.grabFrame()). You can draw that bitmap to a canvas for export or image processing:
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const track = stream.getVideoTracks()[0];
try {
const imageCapture = new ImageCapture(track);
const bitmap = await imageCapture.grabFrame();
const canvas = document.querySelector("canvas");
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext("2d").drawImage(bitmap, 0, 0);
bitmap.close();
} finally {
stream.getTracks().forEach(track => track.stop());
}
This route suits code already organized around tracks and an image-processing pipeline. Check that the target browsers support the API before depending on it. Canvas capture from a video element is the more straightforward path when you need a broadly understandable still-photo workflow, easy preview, or cropping. Neither method has a universal latency, frame-rate, or image-quality guarantee; those depend on the device, browser, stream, and application.
Handle permission, device, and iframe failures
Permission denied or blocked
If the user declines access or browser settings block the camera, the request rejects, commonly with NotAllowedError. Explain that the page needs camera permission and let the user retry after changing the browser’s site permissions. Do not repeatedly prompt or leave the interface appearing to capture when it cannot.
No matching camera
NotFoundError can indicate that no video input matching the request is available. Offer a useful message and, if the application supports it, allow the user to connect or select a different camera.
Insecure page or unavailable API
Camera access requires a secure context. Deploy over HTTPS, or use localhost during development. If navigator.mediaDevices is missing, check the page context and browser support before calling the method.
Embedded page
For camera use inside an iframe, the top-level page must delegate camera access through Permissions Policy, for example with an appropriate allow="camera" attribute and/or policy header. A child page cannot grant itself permission if the embedding page disallows it.
Video has no dimensions yet
A successful permission request does not guarantee that the first frame is ready at the instant your code runs. Wait for metadata or check that both video.videoWidth and video.videoHeight are nonzero before drawing. Keep capture disabled until that condition holds; otherwise drawImage() may have nothing useful to draw.
Camera stays active after capture
A still capture does not stop the live stream. Call stop() on every stream track when the camera is no longer needed, including when the user closes the capture screen or the application exits that workflow. This releases the camera rather than leaving it active in the background.
Rank #4
Performance, reliability, and cost considerations
Capturing a frame locally avoids sending video to a screenshot service: the browser draws pixels from the user’s live camera stream. The practical cost is the work your app performs to keep the stream active, allocate canvas memory, encode the image, and optionally upload it. Reuse the canvas where appropriate, avoid needlessly large output dimensions, and stop tracks promptly. For repeated captures, decide whether the stream should remain open between shots based on the user’s workflow and privacy expectations.
Do not promise a fixed capture delay or quality from the API alone. Camera hardware, selected constraints, browser implementation, lighting, and encoding choices affect the result. Measure performance on the actual supported devices and define a fallback for a rejected permission request or an unavailable camera.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
The JavaScript method above captures a frame from a user’s webcam. If what you need is a screenshot of a website rather than a webcam frame, ScreenshotNeo provides a website screenshot API and MCP server for developers. A single GET request can return a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Sign up for ScreenshotNeo free: 1,000 screenshots a month, no card required.
Best Value
Frequently Asked Questions
Does getUserMedia capture an image by itself?
No. It gives your page a live MediaStream; draw a video frame to canvas or use ImageCapture.grabFrame() to obtain a still image.
Can I capture a frame without showing a video element?
The frame still needs to come from the stream. You can keep a video element visually hidden and draw its frame to canvas, provided it has loaded video dimensions.
What does grabFrame() return?
It returns an ImageBitmap when successful; draw it to a canvas if you need a PNG, JPEG, or other exported image.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

