# Screen Capture API (getDisplayMedia)

> getDisplayMedia() prompts for a screen, window, or tab and returns a MediaStream: every option, every exception the W3C spec defines, feature-detected examples.

`navigator.mediaDevices.getDisplayMedia()` opens the browser's picker for a screen, window, or tab and resolves with a `MediaStream` carrying exactly one video track and at most one audio track, ready for `MediaRecorder`, a `<video>` element, or an `RTCPeerConnection`. Unlike `getUserMedia()`, the grant is never persisted: every call shows the picker again, and the page cannot enumerate display sources or preselect one.

Chrome 72, Edge 79, Firefox 66, and Safari 13 on macOS implement it on desktop (Edge 17 to 79 exposed an earlier version on `navigator`). Chrome for Android 72 to 88 and Firefox for Android 66 to 79 exposed the method but rejected every call with `NotAllowedError` ([Chromium bug 40418135](https://crbug.com/40418135)); current Chrome for Android, Firefox for Android, and Safari on iOS do not expose it at all (BCD `api.MediaDevices.getDisplayMedia`). The capture options beyond `video` and `audio` are Chromium-only.

## Syntax

```js
navigator.mediaDevices.getDisplayMedia()
navigator.mediaDevices.getDisplayMedia(options)
```

Returns a `Promise<MediaStream>`. The call needs transient activation, a document that is fully active and has focus, a secure context, and the `display-capture` Permissions Policy, whose default allowlist is `'self'`; a cross-origin iframe needs `allow="display-capture"`. The browser consumes the activation, so a second call needs a second click.

## Parameters

The single optional argument is a `DisplayMediaStreamOptions` dictionary. `video` and `audio` come from the W3C specification; the remaining members are hints the user agent may ignore, and only Chromium implements them (versions from BCD).

| Member | Type | Required | Description |
|---|---|---|---|
| `video` | `boolean` or `MediaTrackConstraints` | No, default `true` | `false` rejects with `TypeError`. A constraints object may carry `displaySurface` (`"browser"`, `"window"`, `"monitor"`) as a hint plus `width`, `height`, `frameRate`, `aspectRatio`, and `cursor`; constraints are applied after the user has chosen, they do not filter the picker. `min`, `exact`, and `advanced` are rejected. |
| `audio` | `boolean` or `MediaTrackConstraints` | No, default `false` | Asks for an audio track; the browser may return video only. Chrome 74 captures system audio on Windows and ChromeOS and tab audio elsewhere. |
| `controller` | `CaptureController` | No | Lets the page keep or move focus after the user picks a tab or window via `setFocusBehavior()`; one controller per call. Chrome 109. |
| `preferCurrentTab` | `boolean` | No, default `false` | Puts the calling tab first in the picker. Chrome 94. |
| `selfBrowserSurface` | `"include"` or `"exclude"` | No | Whether the calling tab appears in the picker at all; Chrome 112 defaults to `"exclude"` (107 to 111 defaulted to `"include"`). |
| `surfaceSwitching` | `"include"` or `"exclude"` | No | Whether Chrome shows its "Share this tab instead" control during capture. Chrome 107. |
| `systemAudio` | `"include"` or `"exclude"` | No | Whether the picker offers system audio when a monitor is chosen. Chrome 105. |
| `windowAudio` | `"exclude"`, `"window"`, or `"system"` | No | Audio offered when a window is chosen; Chrome 141 accepts `"exclude"` and `"system"` only. |
| `monitorTypeSurfaces` | `"include"` or `"exclude"` | No | Whether whole screens are offered. Chrome 119. |

## Exceptions

The promise rejects with one of the following. Chrome's messages are quoted from `media_devices.cc`.

| Exception | Condition |
|---|---|
| `InvalidStateError` | No transient activation (Chrome: `getDisplayMedia() requires transient activation (user gesture).`); the document is not fully active or does not have focus; the supplied `controller` was already used (Chrome: `A CaptureController object may only be used with a single getDisplayMedia() invocation.`). |
| `TypeError` | `video: false`, or a constraint set containing `advanced`, `min`, or `exact`. |
| `OverconstrainedError` | A `max` constraint is below the property's floor, or applying the constraints to the chosen surface fails. |
| `NotAllowedError` | The user dismissed the picker, the permission state is `"denied"`, the OS or an enterprise policy forbids capture, or the `display-capture` Permissions Policy denies the document (Chrome: `Access to the feature "display-capture" is disallowed by permissions policy.`). |
| `NotFoundError` | No source of a requested type exists. |
| `NotReadableError` | The user granted access but an OS-level lock prevented reading the surface. |
| `AbortError` | Device access failed for a reason not covered above. |

:::observed
Calling `getDisplayMedia()` from a `setTimeout` callback in Chrome rejects with `InvalidStateError: getDisplayMedia() requires transient activation (user gesture).`, and the same call inside a cross-origin iframe without `allow="display-capture"` rejects with `NotAllowedError: Access to the feature "display-capture" is disallowed by permissions policy.` Both strings, and the `CaptureController` reuse message quoted in the table, are thrown from Chromium's [`media_devices.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/mediastream/media_devices.cc) (chromium.googlesource.com).
:::

## Examples

Each example starts from a button click so the activation is present, checks for the method before calling it, and shows what the page does when capture is unavailable or refused.

### Previewing the chosen surface with a fallback message

Detect `getDisplayMedia` on `navigator.mediaDevices`, which is itself absent on insecure origins. A missing method covers Safari on iOS and every Android browser; a `NotAllowedError` after the picker is the user changing their mind and should not be reported as a failure.

```js
const button = document.querySelector("#share-screen");
const preview = document.querySelector("video#preview");
const notice = document.querySelector("#capture-notice");

if (!navigator.mediaDevices?.getDisplayMedia) {
  button.disabled = true;
  notice.textContent = "Screen sharing is not available in this browser.";
} else {
  button.addEventListener("click", async () => {
    try {
      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: { displaySurface: "window" },
        audio: false,
      });
      preview.srcObject = stream;
      stream.getVideoTracks()[0].addEventListener("ended", () => {
        preview.srcObject = null; // user clicked the browser's "Stop sharing" control
      });
    } catch (err) {
      notice.textContent = err.name === "NotAllowedError" ? "Sharing cancelled." : `${err.name}: ${err.message}`;
    }
  });
}
```

The `ended` event is the only signal that the user stopped sharing from the browser's own UI; without the listener the `<video>` keeps showing the last frame.

### Recording a tab to WebM with `MediaRecorder`

Ask for the current tab with `preferCurrentTab` (a Chrome 94 hint other browsers ignore) and audio, then feed the stream to `MediaRecorder`. Stop the recorder when the track ends so the file is finalised even if the user ends sharing from the browser toolbar rather than your button.

```js
async function recordTab() {
  if (!navigator.mediaDevices?.getDisplayMedia || typeof MediaRecorder === "undefined") {
    return null;
  }
  const stream = await navigator.mediaDevices.getDisplayMedia({
    video: true,
    audio: true,
    preferCurrentTab: true,
  });
  const recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
  const chunks = [];
  recorder.addEventListener("dataavailable", (event) => chunks.push(event.data));

  const done = new Promise((resolve) => {
    recorder.addEventListener("stop", () => resolve(new Blob(chunks, { type: "video/webm" })));
  });
  stream.getVideoTracks()[0].addEventListener("ended", () => recorder.stop());
  recorder.start(1000);
  return { stop: () => recorder.stop(), done };
}
```

Check `stream.getAudioTracks().length` before promising users an audio track: Firefox and Safari return video only, and Chrome on macOS and Linux only carries audio for a tab, not a window or screen.

### Keeping focus on your page with `CaptureController`

By default Chrome focuses the captured tab or window after the picker closes, which hides the controls the user just clicked. A `CaptureController` created before the call can ask Chrome to stay put; the method must be called before the promise resolves, and other browsers simply lack the constructor.

```js
async function startCaptureKeepingFocus() {
  const options = { video: true };
  let controller = null;
  if ("CaptureController" in window) {
    controller = new CaptureController();
    options.controller = controller;
  }
  const stream = await navigator.mediaDevices.getDisplayMedia(options);
  if (controller && stream.getVideoTracks()[0].getSettings().displaySurface !== "monitor") {
    controller.setFocusBehavior("no-focus-change");
  }
  return stream;
}
```

`setFocusBehavior()` is ignored for monitors, which have nothing to focus, and throws `InvalidStateError` once the specification's "finalize focus decision" step has run, which happens in a task queued right after the promise resolves; call it synchronously after `await`.

## See also

- [WebRTC](/reference/capabilities/webrtc/), sending the captured stream to another peer
- [WebCodecs](/reference/capabilities/webcodecs/), encoding captured frames without `MediaRecorder`
- [Document Picture-in-Picture](/reference/capabilities/document-picture-in-picture/), keeping capture controls visible while the user works elsewhere
- [Screen Capture: getDisplayMedia() method](https://www.w3.org/TR/screen-capture/#dom-mediadevices-getdisplaymedia) (w3.org)
- [Screen Capture: DisplayMediaStreamOptions dictionary](https://www.w3.org/TR/screen-capture/#dom-displaymediastreamoptions) (w3.org)
- [Better screen sharing with Conditional Focus and the other screen-sharing controls](https://developer.chrome.com/docs/web-platform/screen-sharing-controls/) (developer.chrome.com)
- [Chromium bug 40418135: getDisplayMedia on Android](https://crbug.com/40418135) (crbug.com)