How to Capture a Webpage With JavaScript, Including Video Elements
Capture a tab, screen, video element, or canvas with JavaScript, save stills, and record streams while handling permissions, browser limits, and privacy.
Direct answer: JavaScript browser capture APIs capture rendered pixels or media streams; they do not serialize an arbitrary DOM into a screenshot. Use navigator.mediaDevices.getDisplayMedia() when the user should choose a tab, window, or display. Use HTMLMediaElement.captureStream() to capture a rendered <video> element where supported. Use canvas.captureStream() for canvas animations. A captured stream can be previewed, recorded with MediaRecorder, or converted into a still with ImageCapture.grabFrame() and a canvas.
Display capture requires a secure context, a user gesture, a browser permission prompt, and an explicit surface selection. Stop every track when finished. A selected screen can contain passwords or other private material, so make the surface and stop control obvious.
1. Choose the capture route
| Goal | API | Main trade-offs |
|---|---|---|
| Capture a tab, window, or display | getDisplayMedia() |
User chooses and authorizes a surface; secure context and browser support are required. |
| Capture a video element | video.captureStream() |
Targets that element directly, but support is limited and varies by browser. |
| Capture one still frame | getDisplayMedia() plus ImageCapture.grabFrame() |
The still comes from a live captured track; timing and crop support vary. |
| Record a canvas animation | canvas.captureStream() |
Canvas must remain origin-clean for pixel operations; encoding support varies. |
See the Screen Capture API overview and the getDisplayMedia() reference for browser-specific details.
2. Capture the current tab, window, or screen
Call getDisplayMedia() from a click or other user action. The browser opens its chooser; your code cannot silently select a surface.
<button id="start">Start capture</button>
<button id="stop" disabled>Stop</button>
<video id="preview" autoplay playsinline muted style="max-width:100%"></video>
<script>
const start = document.querySelector('#start');
const stop = document.querySelector('#stop');
const preview = document.querySelector('#preview');
let stream;
start.addEventListener('click', async () => {
try {
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
preview.srcObject = stream;
stop.disabled = false;
start.disabled = true;
stream.getVideoTracks()[0].addEventListener('ended', finish);
} catch (error) {
console.error('Capture was not started:', error);
}
});
stop.addEventListener('click', finish);
function finish() {
if (stream) {
for (const track of stream.getTracks()) track.stop();
stream = undefined;
}
preview.srcObject = null;
stop.disabled = true;
start.disabled = false;
}
</script>
Set audio: true only when you need system or tab audio. Audio availability and chooser options differ by browser and operating system. The MDN screen-capture guide documents the preview and track-stop flow.
Useful display-capture options
videois required and may be a boolean or constraint object.audiorequests an audio track, but does not guarantee one.- Browser-specific hints can influence the chooser, but they do not bypass it or grant permission.
- Inspect
stream.getVideoTracks()[0].getSettings()to learn the actual width, height, frame rate, and display surface returned.
3. Turn a captured frame into a PNG or JPEG
Wait for the preview video to have dimensions, construct an ImageCapture from the video track, and draw the bitmap to a canvas.
async function captureStill(stream, type = 'image/png', quality = 0.92) {
const track = stream.getVideoTracks()[0];
const video = document.querySelector('#preview');
if (video.readyState < 2) {
await new Promise(resolve => video.addEventListener('loadeddata', resolve, { once: true }));
}
const imageCapture = new ImageCapture(track);
const bitmap = await imageCapture.grabFrame();
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
return await new Promise((resolve, reject) => {
canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('toBlob failed')), type, quality);
});
}
// Example after stream has been created:
const blob = await captureStill(stream, 'image/png');
const download = document.createElement('a');
download.href = URL.createObjectURL(blob);
download.download = 'capture.png';
download.click();
URL.revokeObjectURL(download.href);
grabFrame() captures the track’s current frame, so wait until the desired content is visible. Element Capture and Region Capture can narrow what is captured in browsers that implement them; see MDN’s Element and Region Capture guide.
4. Capture a video element
HTMLMediaElement.captureStream() exposes the rendered output of an <video> or <audio> element as a MediaStream. It is marked limited availability, so feature-detect it and provide a fallback.
const video = document.querySelector('video#movie');
if (!video.captureStream) {
throw new Error('This browser does not support HTMLMediaElement.captureStream()');
}
const mediaStream = video.captureStream();
const recorder = new MediaRecorder(mediaStream);
const chunks = [];
recorder.ondataavailable = event => {
if (event.data.size) chunks.push(event.data);
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: recorder.mimeType || 'video/webm' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'video-element.webm';
link.click();
};
video.play();
recorder.start();
setTimeout(() => recorder.stop(), 10000);
Check the captureStream() compatibility table for your browser matrix. A media element can also be subject to autoplay policy; call play() after a user gesture when necessary.
5. Capture and record a canvas animation
const canvas = document.querySelector('canvas');
const stream = canvas.captureStream(30); // requested frame rate
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
? 'video/webm;codecs=vp9'
: 'video/webm';
const recorder = new MediaRecorder(stream, { mimeType });
const chunks = [];
recorder.ondataavailable = event => event.data.size && chunks.push(event.data);
recorder.onstop = () => {
const blob = new Blob(chunks, { type: recorder.mimeType });
// Upload blob or create a download URL here.
};
recorder.start(1000); // emit data roughly every second
// Later: recorder.stop(); stream.getTracks().forEach(track => track.stop());
canvas.captureStream() can throw a SecurityError when the canvas is not origin-clean, such as after drawing cross-origin images or video without appropriate CORS headers. See the canvas captureStream() reference.
6. Record streams reliably with MediaRecorder
Use MediaRecorder.isTypeSupported() before choosing a MIME type. Store all dataavailable chunks and combine them when recording stops; one chunk is not necessarily independently playable.
function recordStream(stream, seconds = 5) {
const candidates = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm'
];
const mimeType = candidates.find(MediaRecorder.isTypeSupported);
if (!mimeType) throw new Error('No supported recording format');
return new Promise((resolve, reject) => {
const recorder = new MediaRecorder(stream, { mimeType });
const chunks = [];
recorder.ondataavailable = e => e.data.size && chunks.push(e.data);
recorder.onerror = e => reject(e.error || new Error('Recorder error'));
recorder.onstop = () => resolve(new Blob(chunks, { type: mimeType }));
recorder.start(1000);
setTimeout(() => recorder.stop(), seconds * 1000);
});
}
The MediaStream Recording API guide covers chunk assembly and recorder events. If you need a different container or codec, check support rather than assuming MP4 is available.
7. Cropping, element targeting, and layout changes
Display capture starts with the entire selected surface. Region or element capture can restrict a track where supported. Layout changes, scrolling, browser zoom, device pixel ratio, and a moving target can change the captured pixels between frames. For a stable still, wait for fonts, images, and video playback to settle before calling grabFrame().
Do not treat a captured stream as a DOM snapshot. It contains rendered output, including animations and video frames, but not hidden nodes, event handlers, semantic structure, or CSS rules as data.
8. Permissions, privacy, and browser constraints
- Serve the page from HTTPS (localhost is generally used for local development).
- Start capture from a user action; browsers reject unsolicited prompts.
- Explain which surface to choose and provide a visible Stop button.
- Stop every track on completion and when the source ends.
- Warn users that a display surface may show passwords, private messages, or unrelated windows.
- Test screen audio separately on each target operating system and browser.
9. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
NotAllowedError |
User cancelled, permission was denied, or the call was not user initiated. | Call from a click, explain the chooser, and handle cancellation without retry loops. |
SecurityError |
Insecure context or a tainted canvas. | Use HTTPS and keep cross-origin media out of pixel-readable canvases unless CORS is configured. |
| Blank or black frame | Video metadata is not ready, playback is paused, or the selected surface is unavailable. | Wait for loadeddata, call play() after interaction, and inspect track settings. |
captureStream is not a function |
The browser does not implement that media-element API. | Feature-detect it; use display capture or a server-side capture path instead. |
| Recording will not play | Unsupported MIME type or an individual chunk was treated as a complete file. | Use isTypeSupported() and combine all chunks into one Blob. |
| Audio is missing | The chooser or platform did not provide an audio track. | Request audio, inspect stream.getAudioTracks(), and verify platform support. |
| Capture stops unexpectedly | The user clicked the browser’s stop-sharing control or the source ended. | Listen for the track’s ended event and reset your UI. |
10. Performance, reliability, and cost
- Higher capture resolution and frame rate increase memory, CPU, upload size, and recorder work.
- Use a reasonable
canvassize and frame rate for previews; capture full resolution only when required. - Emit recorder chunks periodically for long sessions so memory does not grow without bound.
- Revoke object URLs after downloads and stop tracks to release camera, display, and audio resources.
- For repeatable automated captures, browser permission prompts and user-selected surfaces make a client-only approach difficult to run unattended.
11. Or skip the browser setup
If you need a saved webpage image or PDF without opening a chooser, ScreenshotNeo provides a GET endpoint at https://api.screenshotneo.com/v1/shot. See the ScreenshotNeo API documentation for all options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. ScreenshotNeo also offers an MCP server so Claude, Cursor, and other MCP clients can take screenshots, inspect pages, and capture PDFs. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and get 1,000 screenshots each month with no card.
12. FAQ
Can JavaScript screenshot a page without asking the user?
Not with getDisplayMedia(). The browser requires a chooser and permission. Use a server-side screenshot API for unattended captures.
Can I capture only one DOM element?
Element or Region Capture may restrict a display-capture track in supporting browsers. Otherwise, capture the surface and crop a frame on a canvas.
Does video capture include the original video file?
No. It captures rendered frames from the media element. The resulting stream is recorded using a browser-supported codec and container.
Why can’t I read pixels from a cross-origin video?
Drawing cross-origin media can taint a canvas. Without appropriate CORS, pixel reads and related capture operations are blocked for security.
What should I use for automated screenshots in CI?
A browser automation or screenshot API avoids interactive display permission. ScreenshotNeo is a direct HTTP option when you need clean, repeatable page images or PDFs.


