Introduction to the Screen Capture API
The Screen Capture API is a modern browser-based interface that allows web applications to capture the contents of a user's screen, application window, or browser tab for use within the application. Introduced as part of the Media Capture and Streams specification, this API provides a secure, user-consented pathway for developers to access display media streams programmatically.
Unlike older approaches that relied on browser extensions or third-party plugins, the Screen Capture API—exposed through the getDisplayMedia() method—offers a native, cross-platform solution. It returns a MediaStream object containing video tracks of the user's selected display surface, which can then be displayed in a <video> element, recorded with the MediaRecorder API, or transmitted via WebRTC to remote peers.
The API is part of the broader MediaStreams ecosystem, meaning any stream obtained through screen capture can be combined with microphone audio, processed through the Web Audio API, or manipulated on a canvas element. This flexibility makes it a cornerstone technology for video conferencing, collaborative whiteboarding, remote desktop assistance, and browser-based screen recording tools.
Why Screen Capture Matters in Modern Web Applications
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Screen capture capabilities unlock a range of powerful use cases that were previously difficult or impossible to implement purely on the web without native software installation:
- Video Conferencing & Screen Sharing: Applications like Google Meet, Zoom Web, and Microsoft Teams use screen capture to enable participants to share presentations, code editors, or entire desktops during calls.
- Browser-Based Screen Recording: Online tools like Loom, Screencastify, and Canva's recording feature allow users to record tutorials, demos, and product walkthroughs directly from the browser with no download required.
- Remote Assistance & Co-Browsing: Support platforms can view a user's screen (with explicit consent) to diagnose issues, guide them through workflows, or collaboratively edit documents.
- E-Learning & Assessment: Educational platforms can monitor student activity during proctored exams or capture coding exercises for instructor review.
- Web-Based Broadcasting: Live streaming platforms can capture browser game sessions, design tool workflows, or coding sessions for audience viewing.
By providing a standardized API, browsers eliminate the fragmentation caused by vendor-specific solutions. Developers can write a single code path that works across Chrome, Firefox, Edge, and Safari (with appropriate handling for platform nuances), reducing development time and ensuring consistent user experiences.
Browser Support and Feature Detection
Before implementing screen capture, it's essential to verify that the user's browser supports the API. The Screen Capture API has been available in Chrome since version 72, Firefox since version 66, Edge since version 79, and Safari since version 12.1. However, the exact capabilities and supported options vary between implementations.
Use feature detection rather than user-agent sniffing to determine availability:
// Check if the Screen Capture API is available
const isScreenCaptureSupported = () => {
return typeof navigator.mediaDevices?.getDisplayMedia === 'function';
};
// More granular check with potential error handling
const checkScreenCaptureSupport = () => {
if (!navigator.mediaDevices) {
return { supported: false, reason: 'MediaDevices API not available' };
}
if (typeof navigator.mediaDevices.getDisplayMedia !== 'function') {
return { supported: false, reason: 'getDisplayMedia not supported' };
}
return { supported: true, reason: null };
};
// Usage
if (!isScreenCaptureSupported()) {
console.warn('Screen Capture API is not available in this browser');
// Provide fallback UI or instructions for the user
}
For Safari users, additional considerations apply. Safari's implementation requires the user to interact with the browser's built-in screen recording controls, and certain constraints like video.frameRate may behave differently. Always test your implementation across target browsers and provide graceful degradation where full support is unavailable.
How to Request Screen Capture
The core of the Screen Capture API is the getDisplayMedia() method. It takes an optional constraints object and returns a Promise that resolves to a MediaStream if the user grants permission, or rejects if the user denies the request or an error occurs.
Basic Screen Capture Request
// Simple screen capture request with default settings
async function startScreenCapture() {
try {
const displayStream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
console.log('Screen capture stream obtained:', displayStream);
return displayStream;
} catch (error) {
console.error('Screen capture request failed:', error);
// Handle specific error types
switch (error.name) {
case 'AbortError':
console.log('User cancelled the screen selection dialog');
break;
case 'NotAllowedError':
console.log('Permission denied by the user');
break;
case 'NotFoundError':
console.log('No display device found to capture');
break;
case 'NotReadableError':
console.log('Hardware or OS error prevented capture');
break;
case 'OverconstrainedError':
console.log('Requested constraints cannot be satisfied');
break;
case 'TypeError':
console.log('Invalid constraints object provided');
break;
default:
console.log('Unknown error occurred:', error.message);
}
throw error; // Re-throw or handle gracefully
}
}
// Call the function
const stream = await startScreenCapture();
Requesting Screen Capture with Audio
You can request system audio alongside the video track. Note that audio support varies by browser and operating system. Chrome supports system audio capture on Windows, macOS, and ChromeOS, while Firefox's support is more limited.
async function startScreenCaptureWithAudio() {
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30 }
},
audio: true // Request system audio
});
// Check if audio track was actually provided
const audioTracks = stream.getAudioTracks();
if (audioTracks.length > 0) {
console.log('System audio capture active');
} else {
console.log('No system audio available or supported');
}
return stream;
} catch (error) {
console.error('Capture failed:', error);
throw error;
}
}
Controlling Display Surface Type
The constraints object accepts a video.displaySurface hint (where supported) to suggest which type of display surface the browser's picker should prefer:
// Suggest preferred display surface type
async function captureSpecificSurface() {
const constraints = {
video: {
displaySurface: 'window' // Prefer application windows over tabs or monitors
},
audio: false
};
// Alternative: prefer the entire screen (monitor)
// displaySurface: 'monitor'
// Alternative: prefer browser tabs
// displaySurface: 'browser'
try {
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
return stream;
} catch (error) {
console.error('Capture error:', error);
throw error;
}
}
The displaySurface property accepts three values: 'monitor', 'window', and 'browser'. Note that this is a hint and the browser may still present all available options to the user. Chrome and Edge support this property; Firefox currently does not.
Working with the Captured Stream
Once you have a MediaStream from getDisplayMedia(), you can display it in a <video> element, monitor its tracks, and respond to user-initiated stop events.
Displaying the Stream in a Video Element
<!-- HTML structure -->
<video id="screenPreview" autoplay playsinline muted></video>
<button id="startCaptureBtn">Start Capture</button>
<button id="stopCaptureBtn" disabled>Stop Capture</button>
<script>
// Complete screen capture lifecycle management
class ScreenCaptureManager {
constructor(videoElementId, startBtnId, stopBtnId) {
this.videoElement = document.getElementById(videoElementId);
this.startButton = document.getElementById(startBtnId);
this.stopButton = document.getElementById(stopBtnId);
this.currentStream = null;
this.setupEventListeners();
}
setupEventListeners() {
this.startButton.addEventListener('click', () => this.beginCapture());
this.stopButton.addEventListener('click', () => this.endCapture());
}
async beginCapture() {
try {
this.startButton.disabled = true;
this.startButton.textContent = 'Requesting...';
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false
});
this.currentStream = stream;
// Bind the stream to the video element
this.videoElement.srcObject = stream;
// Listen for the browser's stop sharing button
stream.getVideoTracks()[0].addEventListener('ended', () => {
console.log('User stopped sharing via browser controls');
this.handleStreamEnd();
});
this.stopButton.disabled = false;
this.startButton.textContent = 'Restart Capture';
} catch (error) {
console.error('Failed to start capture:', error);
this.startButton.disabled = false;
this.startButton.textContent = 'Start Capture';
if (error.name === 'AbortError') {
alert('Screen capture was cancelled. Please try again.');
}
}
}
endCapture() {
if (this.currentStream) {
// Stop all tracks in the stream
this.currentStream.getTracks().forEach(track => {
track.stop();
});
this.handleStreamEnd();
}
}
handleStreamEnd() {
// Clear the video element
this.videoElement.srcObject = null;
this.currentStream = null;
// Reset UI state
this.stopButton.disabled = true;
this.startButton.disabled = false;
this.startButton.textContent = 'Start Capture';
}
}
// Initialize
const captureManager = new ScreenCaptureManager(
'screenPreview',
'startCaptureBtn',
'stopCaptureBtn'
);
</script>
Monitoring Track State
The ended event on video tracks fires when the user clicks the "Stop Sharing" button in the browser's UI chrome. Your application should listen for this event and clean up resources appropriately:
// Enhanced track monitoring
stream.getVideoTracks().forEach((track, index) => {
console.log(`Video track ${index}:`, {
label: track.label,
readyState: track.readyState,
muted: track.muted,
settings: track.getSettings()
});
track.addEventListener('ended', () => {
console.log(`Track "${track.label}" has ended`);
// Clean up UI, notify other components
cleanupAfterStreamEnd();
});
track.addEventListener('mute', () => {
console.log(`Track "${track.label}" muted`);
});
track.addEventListener('unmute', () => {
console.log(`Track "${track.label}" unmuted`);
});
});
// Also listen for stream-level events
stream.addEventListener('removetrack', (event) => {
console.log('Track removed from stream:', event.track);
});
Recording Screen Capture with MediaRecorder
Combining the Screen Capture API with the MediaRecorder API enables building in-browser screen recording functionality. This approach is used by countless web-based recording tools to capture video that can be downloaded or uploaded.
Complete Screen Recording Implementation
<!-- HTML -->
<button id="recordBtn">Start Recording</button>
<button id="stopRecordBtn" disabled>Stop Recording</button>
<button id="downloadBtn" disabled>Download Recording</button>
<video id="livePreview" autoplay playsinline muted></video>
<video id="playbackPreview" controls style="display:none;"></video>
<div id="recordingStatus">Not Recording</div>
<script>
class ScreenRecorder {
constructor() {
this.mediaStream = null;
this.mediaRecorder = null;
this.recordedChunks = [];
this.recordingBlob = null;
this.recordBtn = document.getElementById('recordBtn');
this.stopRecordBtn = document.getElementById('stopRecordBtn');
this.downloadBtn = document.getElementById('downloadBtn');
this.livePreview = document.getElementById('livePreview');
this.playbackPreview = document.getElementById('playbackPreview');
this.statusDiv = document.getElementById('recordingStatus');
this.initializeButtons();
}
initializeButtons() {
this.recordBtn.addEventListener('click', () => this.startRecording());
this.stopRecordBtn.addEventListener('click', () => this.stopRecording());
this.downloadBtn.addEventListener('click', () => this.downloadRecording());
}
async startRecording() {
try {
this.recordBtn.disabled = true;
this.statusDiv.textContent = 'Requesting screen access...';
// Step 1: Get display media stream
this.mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30, max: 60 }
},
audio: true
});
// Step 2: Display live preview
this.livePreview.srcObject = this.mediaStream;
// Step 3: Set up MediaRecorder
// Determine supported MIME types
const mimeType = this.getSupportedMimeType();
console.log('Using MIME type:', mimeType);
this.recordedChunks = [];
this.mediaRecorder = new MediaRecorder(this.mediaStream, {
mimeType: mimeType,
videoBitsPerSecond: 2500000 // 2.5 Mbps for good quality
});
// Step 4: Handle data available event
this.mediaRecorder.addEventListener('dataavailable', (event) => {
if (event.data && event.data.size > 0) {
this.recordedChunks.push(event.data);
console.log('Chunk recorded, size:', event.data.size);
}
});
// Step 5: Handle recording stop
this.mediaRecorder.addEventListener('stop', () => {
this.recordingBlob = new Blob(this.recordedChunks, {
type: this.mediaRecorder.mimeType
});
console.log('Recording complete. Blob size:', this.recordingBlob.size);
// Create playback URL
const playbackUrl = URL.createObjectURL(this.recordingBlob);
this.playbackPreview.src = playbackUrl;
this.playbackPreview.style.display = 'block';
this.livePreview.style.display = 'none';
this.downloadBtn.disabled = false;
this.statusDiv.textContent = 'Recording ready for playback';
});
// Step 6: Handle stream end (user clicks browser stop button)
this.mediaStream.getVideoTracks()[0].addEventListener('ended', () => {
console.log('Screen sharing stopped by user');
if (this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
}
this.cleanupStream();
});
// Step 7: Start recording
this.mediaRecorder.start(1000); // Capture in 1-second chunks
this.stopRecordBtn.disabled = false;
this.statusDiv.textContent = '🔴 Recording...';
} catch (error) {
console.error('Failed to start recording:', error);
this.recordBtn.disabled = false;
this.statusDiv.textContent = 'Error: ' + error.message;
if (error.name === 'AbortError') {
alert('Screen capture permission was denied.');
}
}
}
stopRecording() {
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.stop();
this.stopRecordBtn.disabled = true;
this.recordBtn.disabled = false;
this.recordBtn.textContent = 'Record Again';
this.statusDiv.textContent = 'Processing recording...';
}
// Also stop the screen sharing tracks
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
}
}
downloadRecording() {
if (!this.recordingBlob) return;
const url = URL.createObjectURL(this.recordingBlob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `screen-recording-${new Date().toISOString()}.webm`;
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
cleanupStream() {
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
this.livePreview.srcObject = null;
this.recordBtn.disabled = false;
this.stopRecordBtn.disabled = true;
}
getSupportedMimeType() {
// Priority order for codecs
const types = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4'
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
// Fallback to browser default
return '';
}
}
// Initialize recorder
const recorder = new ScreenRecorder();
</script>
Recording with Timed Segments
For long recordings, you may want to split the output into timed segments. The timeslice argument in mediaRecorder.start() controls how frequently dataavailable events fire, but you can also manually request data at specific intervals:
// Advanced recording with manual chunking
class SegmentedRecorder {
constructor(stream) {
this.stream = stream;
this.recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});
this.segments = [];
this.segmentDuration = 30000; // 30 seconds per segment
this.recorder.addEventListener('dataavailable', (event) => {
if (event.data.size > 0) {
this.segments.push(event.data);
console.log(`Segment ${this.segments.length} captured`);
}
});
}
start() {
this.recorder.start(this.segmentDuration);
console.log('Segmented recording started');
}
async stopAndGetSegments() {
return new Promise((resolve) => {
this.recorder.addEventListener('stop', () => {
const blobs = this.segments.map(data =>
new Blob([data], { type: 'video/webm' })
);
resolve(blobs);
});
this.recorder.stop();
});
}
}
Advanced: Display Surface Constraints and Options
The Screen Capture API supports several advanced constraints that give you finer control over the capture experience. These options are particularly useful for applications that need to optimize for specific scenarios like low-latency streaming or high-quality archival recording.
Cursor Capture Options
// Control cursor visibility in the captured stream
async function captureWithCursorOptions() {
const constraints = {
video: {
cursor: 'always' // Options: 'always', 'motion', 'never'
}
};
// 'always' - Cursor is always visible in the capture
// 'motion' - Cursor visible only when moving (default in many browsers)
// 'never' - Cursor is never included in the capture
const stream = await navigator.mediaDevices.getDisplayMedia(constraints);
return stream;
}
Logical Surface Control
The preferCurrentTab property (available in Chrome) allows applications to capture the current browser tab without showing the display surface picker dialog, streamlining the user experience for tab-specific recordings:
// Capture the current tab without showing the picker (Chrome-only)
async function captureCurrentTab() {
// Requires the 'preferCurrentTab' member in getDisplayMedia options
// This is an experimental feature in Chrome
try {
const stream = await navigator.mediaDevices.getDisplayMedia({
video: true,
audio: false,
preferCurrentTab: true // Capture the tab that initiated the request
});
return stream;
} catch (error) {
if (error.name === 'InvalidStateError') {
console.log('preferCurrentTab may not be supported in this context');
// Fall back to standard capture
return await navigator.mediaDevices.getDisplayMedia({ video: true });
}
throw error;
}
}
Combined Constraints Example
// Comprehensive constraints configuration
const advancedConstraints = {
video: {
width: { ideal: 1920, max: 3840 },
height: { ideal: 1080, max: 2160 },
frameRate: { ideal: 30, max: 60 },
cursor: 'motion',
displaySurface: 'monitor',
// Note: logicalSurface is deprecated but may appear in some implementations
},
audio: {
// Audio constraints for system audio
echoCancellation: false, // Not applicable for system audio
noiseSuppression: false,
sampleRate: 44100
}
};
async function startAdvancedCapture() {
const stream = await navigator.mediaDevices.getDisplayMedia(advancedConstraints);
// Inspect the actual constraints applied by the browser
const videoTrack = stream.getVideoTracks()[0];
const actualSettings = videoTrack.getSettings();
console.log('Applied settings:', actualSettings);
return stream;
}
Handling Stream Inactivity and Recovery
Screen capture streams can become inactive for various reasons: the user closes the shared window, the browser tab loses focus, or system-level interruptions occur. Implementing robust recovery logic ensures a smooth user experience.
// Robust stream monitoring with inactivity detection
class ResilientScreenCapture {
constructor() {
this.stream = null;
this.inactivityTimer = null;
this.maxInactivityTime = 10000; // 10 seconds
this.lastActivityTimestamp = Date.now();
}
async startCapture() {
this.stream = await navigator.mediaDevices.getDisplayMedia({
video: { frameRate: { ideal: 15 } }
});
const videoTrack = this.stream.getVideoTracks()[0];
// Monitor frame activity using requestVideoFrameCallback or polling
this.monitorTrackActivity(videoTrack);
// Handle track end
videoTrack.addEventListener('ended', () => {
console.log('Track ended, initiating cleanup');
this.handleCaptureEnd();
});
return this.stream;
}
monitorTrackActivity(track) {
// Poll track state periodically
this.inactivityTimer = setInterval(() => {
const now = Date.now();
if (track.readyState === 'live') {
this.lastActivityTimestamp = now;
} else if (track.readyState === 'ended') {
this.handleCaptureEnd();
}
// Check for extended inactivity
if (now - this.lastActivityTimestamp > this.maxInactivityTime) {
console.warn('Stream appears inactive, attempting recovery');
this.handleCaptureEnd();
}
}, 2000);
}
handleCaptureEnd() {
if (this.inactivityTimer) {
clearInterval(this.inactivityTimer);
this.inactivityTimer = null;
}
// Stop all tracks
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
}
// Notify application layer
this.dispatchEvent('capture-ended', { reason: 'stream-inactive' });
}
dispatchEvent(name, detail) {
window.dispatchEvent(new CustomEvent(name, { detail }));
}
destroy() {
this.handleCaptureEnd();
}
}
Best Practices for Screen Capture Implementation
- Always Request Minimum Necessary Permissions: Only request audio if your application genuinely needs system audio. Each additional permission request increases the cognitive burden on users and may reduce acceptance rates.
- Provide Clear User Guidance: Before invoking
getDisplayMedia(), show a brief explanation of what the capture will be used for and what the user should expect from the browser's picker dialog. - Handle the "Stop Sharing" Event Gracefully: The browser provides a "Stop Sharing" button in its UI chrome. Your application must listen for the
endedevent on video tracks and clean up resources without throwing errors or leaving the UI in an inconsistent state. - Use Reasonable Resolution Constraints: Requesting unnecessarily high resolutions (like 4K) can cause performance issues, especially when combined with encoding for recording or transmission. Start with 1080p at 30fps as a sensible default and allow users to opt into higher quality if needed.
- Clean Up Object URLs: When creating blob URLs for video playback or downloads, always revoke them with
URL.revokeObjectURL()to prevent memory leaks. - Test Across Browsers: Chrome, Firefox, Edge, and Safari each have subtle differences in their Screen Capture implementations. Test your code in all target browsers and maintain browser-specific fallbacks where necessary.
- Respect User Privacy: Never attempt to persist screen capture streams beyond the user's intended session. Inform users about what data is being captured and how it's used.
- Combine with MediaRecorder Thoughtfully: When recording, select appropriate codecs and bitrates.
video/webm;codecs=vp9offers excellent quality-to-size ratios for most use cases.
Security and Privacy Considerations
The Screen Capture API is designed with security at its core. Unlike older screen capture mechanisms, it incorporates several protective measures:
- Explicit User Consent: The browser always presents a dialog requiring the user to select which display surface to share. Applications cannot programmatically choose the surface or bypass this dialog (except with the
preferCurrentTaboption in Chrome, which still requires user activation). - No Persistent Access: Permission for screen capture is ephemeral—it lasts only for the duration of the stream. Once the user stops sharing or the page is closed, the permission is revoked.
- Visible Indicators: Browsers display prominent indicators (colored tabs, status bar icons, or floating UI elements) when screen capture is active, making it difficult for malicious sites to capture without the user's awareness.
- Secure Contexts Only: The API is only available in secure contexts (HTTPS or localhost), preventing insecure networks from intercepting screen data.
- No Read-Back of Browser Chrome: Browser UI elements (address bar, extensions toolbar, system dialogs) are excluded from capture streams, preventing credential theft or UI spoofing attacks.
As a developer, you should also implement additional safeguards: never store raw screen capture data on the server without explicit user consent, provide clear stop controls within your application UI, and consider adding watermarks or session identifiers to recordings for audit purposes.
Conclusion
The Screen Capture API represents a mature, well-designed interface that brings native screen sharing and recording capabilities to the web platform. By mastering getDisplayMedia(), understanding stream lifecycle management, and combining screen capture with the MediaRecorder API, developers can build sophisticated collaboration, recording, and broadcasting features that rival desktop-native applications. The key to successful implementation lies in thoughtful constraint selection, robust error handling, meticulous track lifecycle management, and unwavering respect for user privacy and consent. As browser support continues to expand and the specification evolves with features like conditional focus and improved audio capture, the Screen Capture API will only become more integral to the modern web application toolkit.