ScreenshotNeo

BlogHow-to

How Does Webpage Video Capture Work?

Learn how getDisplayMedia, MediaRecorder, permissions, audio, browser support and WebRTC fit together in webpage video capture.

By the ScreenshotNeo team1 October 20268 min read

Webpage video capture usually starts with navigator.mediaDevices.getDisplayMedia(). The browser opens its own picker so the user can choose a tab, window or screen and approve sharing. The page receives a MediaStream, which it can preview in a <video> element, record with MediaRecorder, or send through WebRTC. A page cannot silently choose a source or reuse a permanent grant. See the MDN getDisplayMedia documentation and the Screen Capture API overview.

1. The capture flow

  1. User action: call the API from a click, key press or another transient user activation.
  2. Permission and source picker: the browser asks the user to choose a display surface. Options can influence capture characteristics, but cannot remove the picker or preselect a source.
  3. MediaStream: the promise resolves with at least a video track for the chosen surface.
  4. Output: preview the stream, record it locally, or transmit it to another peer.
  5. End: the user can stop sharing from browser controls, or code can stop every track with track.stop(). A recorder also ends when its stream ends or stop() is called.

2. Minimal screen-sharing example

This complete page requests a source after a button click, previews it, records it, and downloads a WebM file when sharing stops.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Webpage video capture</title>
<button id="start">Start capture</button>
<button id="stop" disabled>Stop and download</button>
<video id="preview" autoplay muted playsinline controls></video>
<p id="status">Idle</p>
<script>
const start = document.querySelector('#start');
const stop = document.querySelector('#stop');
const preview = document.querySelector('#preview');
const status = document.querySelector('#status');
let stream;
let recorder;
let chunks = [];

function chooseMimeType() {
  const types = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/webm',
    'video/mp4'
  ];
  return types.find(type => MediaRecorder.isTypeSupported(type)) || '';
}

start.addEventListener('click', async () => {
  if (!navigator.mediaDevices?.getDisplayMedia) {
    status.textContent = 'Display capture is not supported in this browser.';
    return;
  }
  try {
    stream = await navigator.mediaDevices.getDisplayMedia({
      video: { frameRate: { ideal: 30, max: 60 } },
      audio: true
    });
    preview.srcObject = stream;
    chunks = [];
    const mimeType = chooseMimeType();
    recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
    recorder.addEventListener('dataavailable', event => {
      if (event.data.size > 0) chunks.push(event.data);
    });
    recorder.addEventListener('stop', () => {
      const blob = new Blob(chunks, { type: recorder.mimeType || 'video/webm' });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = 'capture.webm';
      link.click();
      URL.revokeObjectURL(url);
      status.textContent = 'Recording downloaded.';
    });
    stream.getVideoTracks()[0].addEventListener('ended', () => {
      if (recorder.state !== 'inactive') recorder.stop();
      start.disabled = false;
      stop.disabled = true;
    });
    recorder.start(1000); // emit a chunk every second
    start.disabled = true;
    stop.disabled = false;
    status.textContent = 'Capturing…';
  } catch (error) {
    status.textContent = `${error.name}: ${error.message}`;
  }
});

stop.addEventListener('click', () => {
  if (recorder && recorder.state !== 'inactive') recorder.stop();
  stream?.getTracks().forEach(track => track.stop());
  start.disabled = false;
  stop.disabled = true;
});
</script>
</html>

Run this from https:// or http://localhost. Opening a local file directly may not satisfy the secure-context requirement.

3. Request options and their limits

Option Purpose What to expect
video Required video constraints Can express width, height or frame-rate preferences. The browser and chosen source decide the final values.
audio Request audio Audio availability depends on browser, operating system and selected surface. Requesting it does not guarantee system or microphone audio.
displaySurface Hint such as monitor, window or browser A hint only; it does not bypass the picker.
cursor Control whether the pointer is visible Support and behavior vary by browser.
selfBrowserSurface, surfaceSwitching, preferCurrentTab Influence tab-sharing UX in supporting browsers These affect available choices, never silent selection.

Use feature detection and inspect the returned tracks rather than assuming constraints were applied. track.getSettings() reports the actual width, height, frame rate and display surface when exposed.

4. Preview, record or transmit?

  • Preview: assign stream to video.srcObject for local viewing. No media file needs to leave the device.
  • Record: MediaRecorder emits Blob chunks. Codec and container support vary, so test MediaRecorder.isTypeSupported().
  • Transmit: add the tracks to an RTCPeerConnection and use WebRTC signaling to connect a remote peer. WebRTC requires signaling, connection handling and usually a server-side policy for storage or relays.

5. Audio: what can be captured?

Depending on the browser and selected source, audio may come from a shared tab, a shared window, computer audio or a microphone. Some combinations are unavailable. A robust interface should tell users exactly which audio path is intended and verify stream.getAudioTracks().length after permission. Do not promise that checking audio: true will include system sound or microphone input everywhere. The MDN screen-capture guide documents these source differences.

6. Permissions, security and privacy

  • Secure context: supporting browsers require HTTPS, with localhost commonly allowed for development.
  • User activation: call getDisplayMedia() directly from a user gesture. Calling it after an unrelated timer often produces InvalidStateError.
  • Fresh approval: permission is requested for each call; a site cannot silently reuse a previous selection.
  • Embedded pages: an iframe may need a display-capture Permissions Policy allowance, while the browser still shows its own prompt.
  • Accidental exposure: a shared surface can reveal messages, passwords, account data or other private material. Ask users to close sensitive windows and select only the intended tab or window.

7. Stopping and handling lifecycle events

The user can stop sharing through browser chrome at any time. Listen for the video track’s ended event and finalize the recorder there. Stop all tracks when your UI closes, otherwise the browser may continue showing a sharing indicator. Disable duplicate Start buttons while a capture is active.

8. Browser support and graceful fallback

getDisplayMedia() has limited availability, and MediaRecorder containers and codecs differ. Detect each capability:

const canCapture = !!navigator.mediaDevices?.getDisplayMedia;
const canRecord = typeof MediaRecorder !== 'undefined';
const canWebM = canRecord && MediaRecorder.isTypeSupported('video/webm');

If capture is unavailable, explain that the current browser or context does not support it and offer an alternate workflow, such as an installed desktop recorder or a server-side screenshot for static pages. Keep the fallback specific to the user’s goal; a screenshot API does not replace live screen video.

9. Common errors and fixes

Error or symptom Likely cause Fix
NotAllowedError User denied capture, the call lacked activation, or policy blocked it Retry from a click, explain the permission prompt, and check iframe Permissions Policy.
InvalidStateError The document is not focused or there is no transient user activation Call directly inside the event handler and focus the tab.
TypeError Invalid or unsupported constraints Start with video: true, then add conservative constraints.
NotFoundError or no source No capturable display surface is available Use a supported desktop browser and check operating-system screen-recording permissions.
Video preview is black Stream was not assigned, playback was blocked, or the source ended Set srcObject, use autoplay muted playsinline, and handle ended.
No audio in the file The selected source does not provide audio or the browser omitted it Inspect audio tracks, test the exact browser and source, and offer microphone capture separately when appropriate.
NotSupportedError from MediaRecorder Requested MIME type or codec is unsupported Probe with isTypeSupported() and choose a supported type.
Large memory use All chunks were retained for a long recording Upload chunks incrementally or rotate files instead of keeping an unbounded array.

10. Performance and reliability

  • Choose a frame rate and resolution that match the use case. Higher values increase CPU, memory and output size.
  • Use a timeslice such as recorder.start(1000) so data arrives incrementally. For long sessions, send chunks to durable storage and recover from network failures.
  • Watch MediaRecorder.onerror, dataavailable and track ended events. Show elapsed time and a clear Stop control.
  • Do not assume a fixed frame rate: window occlusion, power saving and browser throttling can change delivery.
  • For WebRTC, handle reconnection and backpressure. A live stream and a local recording have different failure modes.
  • Measure the actual track settings and file size in your target browsers rather than relying on one universal quality or codec assumption.

11. cURL, Python and Node.js notes

The browser API itself runs in JavaScript. cURL and Python cannot open the browser’s permission picker; they are useful for calling a separate capture service. For a website screenshot or PDF, ScreenshotNeo provides a documented HTTP endpoint. See the ScreenshotNeo API documentation.

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)
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}`);

Or skip the browser setup

For a static webpage image or PDF, ScreenshotNeo makes one GET request and returns PNG, JPEG, WebP or PDF. Cookie and consent banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads and cache hits are never billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Use the API docs for options such as full-page capture, element selectors, custom CSS and JavaScript, waits, headers, cookies, caching and signed links. Create a free ScreenshotNeo account.

12. Cost and architecture choices

Browser capture has no per-request API charge, but your application pays in CPU, memory, storage, bandwidth and engineering time. Recording locally reduces upload cost but puts file handling on the user’s device. WebRTC shifts complexity to signaling, relays and storage. A hosted screenshot API is appropriate when you need repeatable page images or PDFs rather than a user’s live display. For ScreenshotNeo, failed loads and cache hits are not billed, and each response reports whether it was billed.

13. FAQ

Can a webpage record a browser tab?

Yes. The user selects a tab in the browser picker, and the page receives its display as a video track. The browser still controls selection and permission.

Can capture start automatically when a page loads?

Normally no. The call requires transient user activation and a fresh browser permission decision.

Does screen capture include the microphone?

Only when the browser and your implementation obtain a microphone track. Display audio and microphone audio are separate concerns.

Can I save the recording as MP4?

Only if the target browser exposes an MP4-compatible MediaRecorder type. Probe support and transcode on a server when necessary.

Is a screenshot API the same as webpage video capture?

No. getDisplayMedia() captures a user’s selected display surface over time. A screenshot API loads a URL and returns a still image or document.

What should I test before shipping?

Test permission denial, browser and operating-system settings, tab/window/screen sources, audio availability, stopping from browser controls, unsupported codecs, long recordings and embedded iframe policy.

Primary references: MDN getDisplayMedia(), MDN Screen Capture API, Using the Screen Capture API, MDN MediaRecorder, and web.dev screen recording guide.