How to Capture a Full Web Page with the MediaDevices getDisplayMedia API
getDisplayMedia captures a selected tab or screen as a stream—not a stitched full-page image. Learn the correct API flow, options, errors, and alternatives.
Short answer: navigator.mediaDevices.getDisplayMedia() does not take a one-shot screenshot of an entire scrollable document. It asks the user to choose a display surface—such as a browser tab, window, or monitor—and returns a live MediaStream of that surface. You can preview, record, or transmit the stream, but the API documentation does not describe it as a method for producing one tall image containing everything below the viewport.
If you need a still image of the complete document, treat that as a different problem from screen sharing. This guide shows the correct getDisplayMedia() implementation, its security and browser requirements, every important option, error handling, and a hosted screenshot option at the end.
See the primary references: MDN getDisplayMedia(), MDN Screen Capture API guide, and the W3C Screen Capture specification.
1. What getDisplayMedia actually captures
The method returns a MediaStream representing a surface selected by the user:
- A browser tab
- A browser window
- An entire monitor or display
The stream contains video and may contain audio. The selected surface is displayed over time, so changes, scrolling, notifications, and other visible content can appear in the stream. A tab can be selected, but the browser still captures the tab’s displayed surface rather than exposing an abstract DOM document.
For a single full-document image, you need a document-rendering workflow that can scroll and stitch or otherwise render the page outside the viewport. Do not promise a tall page screenshot from getDisplayMedia() alone.
2. Requirements before calling the API
- Use a secure context. Serve the page over HTTPS (localhost is generally treated as secure for development).
- Start from transient user activation. Call the method directly from a click, pointer, or keyboard handler. Delaying it until a later timer or promise callback can cause
InvalidStateError. - Expect a browser picker. The browser chooses when and how to show its source-selection UI. Your code cannot silently select a tab or monitor.
- Request permission every time. Screen-capture permission is intentionally explicit for each request; do not design around a permanently saved approval.
- Account for embedding policy. An iframe may need a Permissions Policy that allows
display-capture.
These requirements are part of the privacy model: users must see what they are sharing and actively approve it.
3. Minimal working example
The following page starts capture from a button, previews the stream, and stops every track when the user finishes.
<!doctype html>
<html lang='en'>
<meta charset='utf-8'>
<title>Display capture demo</title>
<button id='start'>Start capture</button>
<button id='stop' disabled>Stop capture</button>
<video id='preview' autoplay muted playsinline controls></video>
<pre id='status'></pre>
<script>
const startButton = document.querySelector('#start');
const stopButton = document.querySelector('#stop');
const video = document.querySelector('#preview');
const status = document.querySelector('#status');
let stream;
startButton.addEventListener('click', async () => {
status.textContent = 'Waiting for a surface selection…';
try {
stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
video.srcObject = stream;
startButton.disabled = true;
stopButton.disabled = false;
status.textContent = 'Capture is active.';
stream.getVideoTracks()[0]?.addEventListener('ended', () => {
stopCapture();
status.textContent = 'Capture ended from the browser or operating system.';
});
} catch (error) {
status.textContent = `${error.name}: ${error.message}`;
console.error(error);
}
});
stopButton.addEventListener('click', stopCapture);
function stopCapture() {
stream?.getTracks().forEach(track => track.stop());
stream = undefined;
video.srcObject = null;
startButton.disabled = false;
stopButton.disabled = true;
}
</script>
The browser does not return a file. It returns live tracks. Attach the stream to a <video> element for preview, pass it to MediaRecorder for a recording, or send it through WebRTC.
4. Options, constraints, and source selection
video is required. It defaults to true; setting it to false rejects with TypeError. audio defaults to false. Requesting audio only makes an audio track possible; the browser, operating system, and selected surface determine whether one is offered.
Common capture hints include:
| Option | Purpose | Important limitation |
|---|---|---|
preferCurrentTab |
Suggests the current tab as a convenient choice. | It does not silently select that tab. |
selfBrowserSurface |
Controls whether the current browser surface may be offered. | Support and defaults vary by browser. |
monitorTypeSurfaces |
Hints about offering monitor surfaces. | The browser still owns the picker. |
systemAudio |
Requests system audio where supported. | An audio track is not guaranteed. |
windowAudio |
Hints about audio associated with a window. | Availability depends on browser and operating system. |
surfaceSwitching |
Allows a browser to offer switching surfaces during capture. | Behavior is browser-dependent. |
Constraints such as width, height, frame rate, and display surface settings configure the resulting track after the user chooses a source. They cannot narrow the picker or force a particular tab. Avoid min and exact constraints in the initial display-capture request; invalid use can produce TypeError or an unsatisfiable constraint error.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
frameRate: { ideal: 30, max: 60 },
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: {
systemAudio: 'include'
},
preferCurrentTab: true,
surfaceSwitching: 'include'
});
Because support for these hints differs, inspect the returned tracks rather than assuming the requested size, frame rate, or audio mode was accepted.
5. Preview, record, and end capture safely
Preview
video.srcObject = stream;
await video.play();
Use autoplay, muted, and playsinline for a reliable local preview. The capture stream may end when the user presses the browser’s stop-sharing control, so listen for the video track’s ended event.
Record a WebM file
const recorder = new MediaRecorder(stream);
const chunks = [];
recorder.addEventListener('dataavailable', event => {
if (event.data.size) chunks.push(event.data);
});
recorder.addEventListener('stop', () => {
const blob = new Blob(chunks, { type: recorder.mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'display-capture.webm';
link.click();
URL.revokeObjectURL(url);
});
recorder.start();
// Later, in response to a Stop button:
recorder.stop();
stream.getTracks().forEach(track => track.stop());
Check MediaRecorder.isTypeSupported() before choosing a codec if your application needs a particular container. Recording support is separate from display-capture support.
6. Common errors and fixes
| Error | Typical cause | Fix |
|---|---|---|
InvalidStateError |
No transient user activation, inactive or unfocused document, or a reused CaptureController. |
Call from the click handler, keep the page focused, and create a fresh controller for each request. |
NotAllowedError |
User denied the picker or the browsing context is not allowed to capture. | Explain why sharing is needed, retry from a user action, and check iframe Permissions Policy. |
NotFoundError |
No screen-video source is available. | Check the operating system’s display environment and retry in a supported desktop context. |
NotReadableError |
An operating-system or hardware failure occurred after selection. | Close other capture applications, verify OS permissions, and try another surface. |
OverconstrainedError |
The selected source cannot satisfy the requested track constraints. | Remove strict constraints and retry with ideals or browser-reported settings. |
TypeError |
Invalid options, video: false, or unsupported min/exact constraints. |
Set video to true or a valid constraint object and simplify the request. |
Always log error.name and error.message. Browser-specific failures can occur beyond this list.
7. Browser support, performance, and privacy
MDN labels getDisplayMedia() as limited availability and not Baseline. Verify the exact behavior in every browser and operating system you support, especially for mobile devices, audio, surface switching, and recording.
- Performance: Higher resolution and frame rate increase CPU, memory, and bandwidth use. Request only the quality your preview, recording, or transport needs.
- Lifecycle: Stop tracks as soon as capture ends. This releases the camera-like system resource and prevents a stale stream from remaining attached.
- Reliability: Treat audio as optional, monitor the
endedevent, and handle picker cancellation as a normal user outcome. - Privacy: A selected monitor can expose notifications, unrelated windows, passwords, or other sensitive content. Make the selected surface clear before capture starts and avoid recording more than necessary.
- Embedding: If the app runs in an iframe, configure the appropriate
Permissions-Policyand test the deployed origin, not only localhost.
8. Or skip the browser setup
If your goal is a clean still screenshot or PDF of a URL, ScreenshotNeo provides a website screenshot API and MCP server. It renders the page server-side and returns PNG, JPEG, WebP, or PDF from one request. Read the ScreenshotNeo API documentation for the complete option list.
Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
cURL
curl -G 'https://api.screenshotneo.com/v1/shot' \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
'https://api.screenshotneo.com/v1/shot',
params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'},
timeout=90,
)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', buffer));
ScreenshotNeo also supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets, custom viewports, retina scale, PDF paper and page-range settings, custom CSS and JavaScript, clicks, selector or network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. The parameter names used by other screenshot APIs also work, which can simplify migration.
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free, and every feature is included on every plan.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
9. FAQ
Can getDisplayMedia capture the entire webpage below the fold?
Not as a documented one-call full-page screenshot API. It captures the selected display surface as a stream. A tab can show a page, but content outside the visible surface is not automatically stitched into one tall image.
Can JavaScript choose the tab without showing a picker?
No. The user must select the source and grant access for each capture request. Options are hints, not silent source-selection controls.
Why did I request audio but receive only video?
Audio depends on the selected surface, operating system, browser, and available audio-sharing mode. Inspect stream.getAudioTracks() instead of assuming an audio track exists.
Is the returned stream a screenshot file?
No. It is a live MediaStream. Preview it, record it with MediaRecorder, or send it through another real-time pipeline.
What should I use for automated URL screenshots?
Use a document-rendering screenshot service such as ScreenshotNeo when you need repeatable server-side captures, full-page output, PDF, cleanup of consent UI, and API automation rather than an interactive user screen-share.


