Introduction to the Screen Capture API
The Screen Capture API is a powerful browser-based interface that allows web applications to capture the contents of a user's screen, a specific application window, or a browser tab. Built on top of the MediaDevices interface, it provides a standardized way to access screen content as a live MediaStream, which can then be recorded, transmitted over a network, or displayed in a video element.
Originally introduced to support screen sharing in web-based video conferencing tools, the API has since become a foundation for a wide range of applications, including remote collaboration, live streaming, automated testing, and AI-powered screen analysis. Because it deals with potentially sensitive user content, the API is designed with privacy and security as first-class concerns.
Why the Screen Capture API Matters
Before this API existed, capturing a user's screen from a web page required browser extensions or third-party plugins, which introduced friction in installation, maintenance, and security. The Screen Capture API eliminates that barrier by providing a native, permission-gated mechanism built directly into the browser.
Key Benefits
- Zero installation: Users can share their screen without installing any additional software or extensions.
- Cross-platform consistency: The same JavaScript code works across Chrome, Edge, Firefox, and Safari (with minor differences in feature support).
- Granular capture options: Users choose exactly what to share — the entire screen, a single window, or a specific browser tab.
- Integration with WebRTC: Captured streams can be sent peer-to-peer with low latency, enabling real-time collaboration.
- Integration with MediaRecorder: Streams can be recorded locally and saved as video files for later playback.
Core Concepts
The Screen Capture API centers around a single method: navigator.mediaDevices.getDisplayMedia(). This method prompts the user to select what they want to share and returns a MediaStream containing video and, optionally, audio tracks. Understanding the surrounding concepts is essential before writing production code.
The MediaStream Object
A MediaStream is a real-time flow of media data. When returned from getDisplayMedia(), it typically contains one video track representing the captured screen content. If the user opts to share audio (such as a browser tab's audio), the stream may also contain one or more audio tracks. Each track is a MediaStreamTrack object that can be individually controlled, stopped, or inspected.
Permission Model
Unlike camera or microphone access, screen capture permissions are not persistent. Every call to getDisplayMedia() triggers a fresh browser-level picker dialog. The user must actively choose what to share each time. This design prevents web pages from silently capturing screen content in the background. If a user dismisses the picker or denies permission, the returned promise rejects with a NotAllowedError.
Secure Context Requirement
The Screen Capture API is only available in secure contexts. This means your page must be served over HTTPS, or accessed via localhost during development. Attempting to call getDisplayMedia() over plain HTTP will result in the method being undefined or throwing an error.
Basic Usage: Capturing the Screen
The simplest way to use the Screen Capture API is to request a display media stream and attach it to a <video> element for playback. Here is a minimal, complete example.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Screen Capture Demo</title>
</head>
<body>
<button id="startBtn">Start Capture</button>
<button id="stopBtn" disabled>Stop Capture</button>
<br><br>
<video id="preview" autoplay muted style="width: 800px; border: 1px solid #ccc;"></video>
<script>
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const preview = document.getElementById('preview');
let displayStream = null;
startBtn.addEventListener('click', async () => {
try {
displayStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
preview.srcObject = displayStream;
startBtn.disabled = true;
stopBtn.disabled = false;
// Automatically stop when the user clicks the browser's native "Stop sharing" button
displayStream.getVideoTracks()[0].addEventListener('ended', () => {
stopCapture();
});
} catch (err) {
console.error('Failed to capture screen:', err);
alert('Screen capture failed: ' + err.message);
}
});
stopBtn.addEventListener('click', stopCapture);
function stopCapture() {
if (displayStream) {
displayStream.getTracks().forEach(track => track.stop());
displayStream = null;
}
preview.srcObject = null;
startBtn.disabled = false;
stopBtn.disabled = true;
}
</script>
</body>
</html>
</code></pre>
Let's break down what happens in this example. When the user clicks "Start Capture," the browser shows its native screen-sharing picker. Once the user selects a source, getDisplayMedia() resolves with a MediaStream. We assign that stream to the video element's srcObject property, which causes the captured content to render in real time. The ended event listener handles the case where the user stops sharing through the browser's native UI rather than our custom button.
Configuring Capture Constraints
The getDisplayMedia() method accepts a constraints object that lets you specify desired video and audio properties. These constraints are hints — the browser may not honor all of them, especially when the user selects a source with fixed dimensions. However, they are useful for optimizing bandwidth and quality.
Video Constraints
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
frameRate: 30,
width: { ideal: 1920 },
height: { ideal: 1080 },
cursor: 'always' // 'always' | 'motion' | 'never'
},
audio: false
});
</code></pre>
The cursor constraint controls whether the mouse cursor appears in the captured video. A value of 'always' includes the cursor at all times, 'motion' shows it only when moving, and 'never' hides it entirely. Support for this constraint varies by browser; Chrome supports it for screen and tab captures.
Audio Capture
Audio capture is particularly useful when sharing a browser tab that plays sound. To request audio, set the audio property to true or pass a constraints object.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
// Check whether audio was actually granted
const audioTracks = stream.getAudioTracks();
if (audioTracks.length === 0) {
console.log('User did not share audio');
} else {
console.log('Audio track label:', audioTracks[0].label);
}
</code></pre>
Note that audio sharing is only available for browser tabs and, in some browsers, the entire screen. Window-level audio capture is generally not supported. Users can also choose to disable audio in the picker even if you request it, so always check the resulting tracks rather than assuming audio is present.
Recording the Captured Stream
One of the most common use cases is recording the screen capture and saving it as a video file. The MediaRecorder API works seamlessly with the stream returned by getDisplayMedia(). Here is a complete recording example.
let mediaRecorder = null;
let recordedChunks = [];
async function startRecording() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 },
audio: true
});
recordedChunks = [];
const mimeType = getSupportedMimeType();
mediaRecorder = new MediaRecorder(stream, { mimeType });
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `screen-recording-${Date.now()}.webm`;
a.click();
URL.revokeObjectURL(url);
};
// Stop recording when the user stops sharing
stream.getVideoTracks()[0].addEventListener('ended', () => {
if (mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
});
mediaRecorder.start(1000); // Collect data in 1-second chunks
console.log('Recording started');
} catch (err) {
console.error('Recording failed:', err);
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}
function getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/webm',
'video/mp4'
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return 'video/webm';
}
</code></pre>
This example demonstrates several important patterns. First, we probe for a supported MIME type because codec support varies across browsers. Second, we pass a timeslice value of 1000 to start(), which causes ondataavailable to fire every second with accumulated data. This is more memory-efficient than waiting for the entire recording to finish. Finally, we listen for the ended event on the video track so that if the user stops sharing via the browser UI, we gracefully finalize the recording and trigger the download.
Streaming Over WebRTC
For real-time screen sharing between users, the captured stream can be added to a WebRTC RTCPeerConnection. The stream is treated identically to a camera stream from a media perspective.
const peerConnection = new RTCPeerConnection(rtcConfig);
async function shareScreen() {
const displayStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 },
audio: true
});
// Add all tracks from the display stream to the peer connection
displayStream.getTracks().forEach(track => {
peerConnection.addTrack(track, displayStream);
});
// Renegotiate the connection to inform the remote peer of the new tracks
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
// Send the offer to the remote peer via your signaling server
signalingChannel.send({ type: 'offer', sdp: offer });
}
</code></pre>
In a production WebRTC application, you would typically replace an existing camera track with the screen capture track when the user starts sharing, then switch back when they stop. This avoids creating a separate peer connection and simplifies the signaling flow.
Handling Track Lifecycle Events
Screen capture tracks have a lifecycle that differs from camera tracks. The most important event to handle is ended, which fires when the user stops sharing through the browser's native controls. Failing to handle this event leaves your application in an inconsistent state.
const videoTrack = stream.getVideoTracks()[0];
videoTrack.addEventListener('ended', () => {
console.log('User stopped sharing via browser controls');
cleanupResources();
});
videoTrack.addEventListener('mute', () => {
console.log('Track is muted (e.g., screen locked or minimized)');
});
videoTrack.addEventListener('unmute', () => {
console.log('Track resumed');
});
// You can also check track properties
console.log('Track label:', videoTrack.label);
console.log('Track settings:', videoTrack.getSettings());
console.log('Track constraints:', videoTrack.getConstraints());
</code></pre>
The getSettings() method returns the actual values the browser applied, which may differ from what you requested. This is useful for logging, debugging, and adapting your UI to the real capture parameters.
Browser Compatibility and Feature Detection
While the Screen Capture API is widely supported, there are differences in feature availability. Always use feature detection before calling the API.
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
console.error('Screen Capture API is not supported in this browser');
// Show a fallback message or redirect to an extension-based solution
} else {
// Proceed with screen capture
}
</code></pre>
Here is a summary of current browser support:
- Chrome / Edge: Full support including audio capture for tabs, cursor control, and
MediaRecorder integration.
- Firefox: Supports screen and window capture. Audio capture support is more limited and may require enabling flags.
- Safari: Supports screen capture since Safari 13. Audio capture support is limited compared to Chrome.
Best Practices
Always Handle Errors Gracefully
Users can deny permission, dismiss the picker, or encounter hardware issues. Wrap every getDisplayMedia() call in a try-catch block and provide clear feedback.
try {
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
// use stream
} catch (err) {
if (err.name === 'NotAllowedError') {
showUserMessage('You denied screen capture permission. Please try again.');
} else if (err.name === 'NotFoundError') {
showUserMessage('No screen capture source was found.');
} else if (err.name === 'NotReadableError') {
showUserMessage('The selected source could not be captured. It may be in use by another application.');
} else {
showUserMessage('An unexpected error occurred: ' + err.message);
}
}
</code></pre>
Stop Tracks When Done
Every track you obtain continues consuming system resources until it is explicitly stopped. Always call track.stop() when the capture is no longer needed. This also dismisses the browser's "sharing" indicator bar, giving users clear visual confirmation that sharing has ended.
Respect User Privacy
Screen capture exposes everything on a user's screen, including personal information, notifications, and other applications. Be transparent about what you are capturing and why. Avoid capturing more than necessary, and never transmit or store captured content without explicit user consent.
Optimize for Performance
Screen capture at high resolutions and frame rates can be CPU-intensive. Request only the quality you need. For example, a code review tool may only need 15 frames per second at 1080p, while a game streaming application may require 60 frames per second. Use constraints to communicate your requirements to the browser.
// Low-bandwidth configuration for remote support
const lowQualityStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 15, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
});
// High-quality configuration for local recording
const highQualityStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 60, width: { ideal: 2560 }, height: { ideal: 1440 } },
audio: true
});
</code></pre>
Provide Clear UI Controls
Always give users an obvious way to start and stop sharing from within your application, in addition to the browser's native controls. This reduces confusion and improves the user experience. Visually indicate when sharing is active, perhaps with a red badge or a pulsing icon.
Advanced Techniques
Capturing a Specific Surface Type
The displaySurface constraint lets you express a preference for the type of surface to capture. This is a hint and the browser may still allow the user to choose a different type.
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
displaySurface: 'browser' // 'monitor' | 'window' | 'browser'
}
});
</code></pre>
Combining Camera and Screen Capture
For presentation-style applications, you may want to overlay a webcam feed on top of a screen capture. You can achieve this with a Canvas element that composites both streams.
async function startCompositeCapture() {
const displayStream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: 30 },
audio: false
});
const cameraStream = await navigator.mediaDevices.getUserMedia({
video: { width: 320, height: 240 },
audio: false
});
const canvas = document.createElement('canvas');
canvas.width = 1920;
canvas.height = 1080;
const ctx = canvas.getContext('2d');
const displayVideo = document.createElement('video');
displayVideo.srcObject = displayStream;
displayVideo.autoplay = true;
displayVideo.muted = true;
const cameraVideo = document.createElement('video');
cameraVideo.srcObject = cameraStream;
cameraVideo.autoplay = true;
cameraVideo.muted = true;
function drawFrame() {
ctx.drawImage(displayVideo, 0, 0, canvas.width, canvas.height);
// Draw camera in bottom-right corner
ctx.drawImage(cameraVideo, canvas.width - 340, canvas.height - 260, 320, 240);
requestAnimationFrame(drawFrame);
}
drawFrame();
const compositeStream = canvas.captureStream(30);
// Use compositeStream for recording or WebRTC
return compositeStream;
}
</code></pre>
This technique uses canvas.captureStream() to turn the composited canvas into a MediaStream, which can then be recorded or transmitted just like any other stream. The requestAnimationFrame loop continuously redraws both video sources onto the canvas, creating a live picture-in-picture effect.
Conclusion
The Screen Capture API has transformed what web applications can achieve without plugins or extensions. By understanding its permission model, constraint system, and track lifecycle, you can build robust screen sharing, recording, and streaming features that work across modern browsers. The key to using it well is respecting the user's privacy and control: always request only what you need, handle errors and interruptions gracefully, and provide clear visual feedback throughout the capture session. With the patterns and techniques covered in this guide, you are equipped to integrate screen capture into your applications in a way that is both powerful and user-friendly.