← Back to DevBytes

Implementing Screen Capture API in Modern Web Applications

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:

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

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:

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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles