# Screen Capture API（getDisplayMedia）

> navigator.mediaDevices.getDisplayMedia() 让用户选择屏幕、窗口或标签页并返回 MediaStream：每个选项、W3C 规范定义的每个异常，以及带特性检测的示例。

`navigator.mediaDevices.getDisplayMedia()` 打开浏览器的屏幕、窗口或标签页选择器，兑现为一个恰好含一条视频轨、至多一条音频轨的 `MediaStream`，可直接交给 `MediaRecorder`、`<video>` 元素或 `RTCPeerConnection`。与 `getUserMedia()` 不同，授权从不持久化：每次调用都会再弹选择器，页面无法枚举显示源，也无法预选。

桌面端的 Chrome 72、Edge 79、Firefox 66 与 macOS 上的 Safari 13 实现了它（Edge 17 到 79 在 `navigator` 上暴露过早期版本）。Android 上的 Chrome 72 到 88 与 Firefox 66 到 79 暴露了该方法，但每次调用都以 `NotAllowedError` 拒绝（[Chromium bug 40418135](https://crbug.com/40418135)）；如今 Android 上的 Chrome、Firefox 与 iOS 上的 Safari 都不再暴露它（BCD `api.MediaDevices.getDisplayMedia`）。`video` 与 `audio` 之外的捕获选项只有 Chromium 实现。

## 语法

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

返回 `Promise<MediaStream>`。调用需要瞬时激活、处于 fully active 且拥有焦点的文档、安全上下文，以及 `display-capture` Permissions Policy（默认允许列表为 `'self'`，跨源 iframe 需要 `allow="display-capture"`）。浏览器会消耗这次激活，第二次调用需要再点一次。

## 参数

唯一的可选参数是 `DisplayMediaStreamOptions` 字典。`video` 与 `audio` 来自 W3C 规范；其余成员是浏览器可以忽略的提示，只有 Chromium 实现（版本号来自 BCD）。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `video` | `boolean` 或 `MediaTrackConstraints` | 否，默认 `true` | 传 `false` 以 `TypeError` 拒绝。约束对象可带作为提示的 `displaySurface`（`"browser"`、`"window"`、`"monitor"`）以及 `width`、`height`、`frameRate`、`aspectRatio`、`cursor`；约束在用户选定之后才应用，不会过滤选择器。`min`、`exact` 与 `advanced` 会被拒绝。 |
| `audio` | `boolean` 或 `MediaTrackConstraints` | 否，默认 `false` | 请求音频轨；浏览器可以只返回视频。Chrome 74 在 Windows 与 ChromeOS 上捕获系统音频，其他平台只捕获标签页音频。 |
| `controller` | `CaptureController` | 否 | 用户选定标签页或窗口后，让页面通过 `setFocusBehavior()` 保留或转移焦点；每次调用一个控制器。Chrome 109。 |
| `preferCurrentTab` | `boolean` | 否，默认 `false` | 把调用方标签页排在选择器最前。Chrome 94。 |
| `selfBrowserSurface` | `"include"` 或 `"exclude"` | 否 | 调用方标签页是否出现在选择器中；Chrome 112 默认 `"exclude"`（107 到 111 默认 `"include"`）。 |
| `surfaceSwitching` | `"include"` 或 `"exclude"` | 否 | 捕获期间 Chrome 是否显示「改为共享此标签页」控件。Chrome 107。 |
| `systemAudio` | `"include"` 或 `"exclude"` | 否 | 选择整个屏幕时选择器是否提供系统音频。Chrome 105。 |
| `windowAudio` | `"exclude"`、`"window"` 或 `"system"` | 否 | 选择窗口时提供的音频；Chrome 141 只接受 `"exclude"` 与 `"system"`。 |
| `monitorTypeSurfaces` | `"include"` 或 `"exclude"` | 否 | 是否提供整个屏幕作为选项。Chrome 119。 |

## 异常

Promise 以下列之一拒绝。Chrome 的消息引自 `media_devices.cc`。

| 异常 | 条件 |
|---|---|
| `InvalidStateError` | 没有瞬时激活（Chrome：`getDisplayMedia() requires transient activation (user gesture).`）；文档不是 fully active 或没有焦点；传入的 `controller` 已被用过（Chrome：`A CaptureController object may only be used with a single getDisplayMedia() invocation.`）。 |
| `TypeError` | `video: false`，或约束集中含 `advanced`、`min` 或 `exact`。 |
| `OverconstrainedError` | 某个 `max` 约束低于该属性的下限，或对所选表面应用约束失败。 |
| `NotAllowedError` | 用户关闭了选择器、权限状态为 `"denied"`、操作系统或企业策略禁止捕获，或 `display-capture` Permissions Policy 拒绝了该文档（Chrome：`Access to the feature "display-capture" is disallowed by permissions policy.`）。 |
| `NotFoundError` | 不存在所请求类型的源。 |
| `NotReadableError` | 用户已授权，但操作系统层面的锁阻止读取该表面。 |
| `AbortError` | 设备访问因上述之外的原因失败。 |

:::observed
在 Chrome 中从 `setTimeout` 回调调用 `getDisplayMedia()`，以 `InvalidStateError: getDisplayMedia() requires transient activation (user gesture).` 拒绝；同一调用放在缺少 `allow="display-capture"` 的跨源 iframe 内，以 `NotAllowedError: Access to the feature "display-capture" is disallowed by permissions policy.` 拒绝。这两条字符串以及表中引用的 `CaptureController` 复用消息，都来自 Chromium 的 [`media_devices.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/mediastream/media_devices.cc)（chromium.googlesource.com）。
:::

## 示例

每个示例都从按钮点击开始以保证激活存在，调用前先检测方法，并给出捕获不可用或被拒绝时页面的做法。

### 预览所选表面，不可用时给出提示

检测 `navigator.mediaDevices` 上的 `getDisplayMedia`；在非安全源上 `mediaDevices` 本身就不存在。方法缺失覆盖了 iOS 上的 Safari 与所有 Android 浏览器；选择器出现之后的 `NotAllowedError` 是用户改了主意，不应当作失败上报。

```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 = "此浏览器不支持屏幕共享。";
} 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; // 用户点了浏览器自己的「停止共享」控件
      });
    } catch (err) {
      notice.textContent = err.name === "NotAllowedError" ? "已取消共享。" : `${err.name}: ${err.message}`;
    }
  });
}
```

`ended` 事件是用户从浏览器自身 UI 停止共享的唯一信号；不监听它，`<video>` 会一直停在最后一帧。

### 用 `MediaRecorder` 把标签页录成 WebM

用 `preferCurrentTab`（Chrome 94 的提示，其他浏览器忽略）请求当前标签页并带音频，再把流交给 `MediaRecorder`。轨道结束时停止录制器，这样即使用户从浏览器工具栏而不是你的按钮结束共享，文件也能收尾。

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

向用户承诺有音频轨之前先看 `stream.getAudioTracks().length`：Firefox 与 Safari 只返回视频，macOS 与 Linux 上的 Chrome 只在共享标签页时带音频，窗口与屏幕都没有。

### 用 `CaptureController` 让焦点留在你的页面

默认情况下，选择器关闭后 Chrome 会把焦点交给被捕获的标签页或窗口，用户刚点过的控件随之被挡住。调用前创建的 `CaptureController` 可以要求 Chrome 停在原处；必须在 Promise 兑现后的那一刻调用，其他浏览器根本没有这个构造函数。

```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()` 会被忽略，因为没有可聚焦的对象；规范的「finalize focus decision」步骤在 Promise 兑现后紧接着排队的任务里执行，此后再调用会抛 `InvalidStateError`，所以 `await` 之后要同步调用。

## 另请参阅

- [WebRTC](/zh/reference/capabilities/webrtc/)，把捕获到的流发给另一端
- [WebCodecs](/zh/reference/capabilities/webcodecs/)，不经 `MediaRecorder` 直接编码捕获帧
- [Document Picture-in-Picture](/zh/reference/capabilities/document-picture-in-picture/)，用户切到别处时让捕获控件保持可见
- [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）