# Document Picture-in-Picture API

> documentPictureInPicture.requestWindow() 打开一个承载任意 HTML 的置顶窗口。本页列出全部选项、它会以哪些异常拒绝，以及带回退分支的示例。

`window.documentPictureInPicture.requestWindow()` 打开一个同源、置顶的窗口，你可以往它的 `document` 里放任意 HTML；旧的 Picture-in-Picture API 只接受单个 `<video>` 元素。这个窗口浮在其他窗口之上，随打开它的页面关闭而关闭，不能导航，每个标签页最多一个。

Chrome 116 与 Edge 116 仅在桌面端提供；BCD 对 Android 上的 Chrome 记录为 `version_added: false`。Firefox 151 在桌面端加入，同样没有 Android 支持。Safari 没有实现（BCD `api.DocumentPictureInPicture`）。

## 语法

```js
documentPictureInPicture.requestWindow()
documentPictureInPicture.requestWindow(options)

documentPictureInPicture.window
documentPictureInPicture.addEventListener("enter", listener)
```

`requestWindow()` 返回 `Promise<Window>`，以新窗口自己的 `Window` 对象兑现；其文档初始为 `about:blank`，但 base URL 继承自打开它的文档，所以相对路径的样式表和图片按打开方页面解析。`window` 取值器在画中画窗口打开期间返回它，否则返回 `null`。`enter` 事件在 `documentPictureInPicture` 上触发，事件对象是 `DocumentPictureInPictureEvent`，其 `window` 属性为新窗口；只有你自己调用 `requestWindow()` 打开的窗口才会触发它。

## 参数

`requestWindow()` 接受一个可选的 `options` 参数，类型为 `DocumentPictureInPictureOptions` 字典，每个成员都可选。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `width` | `unsigned long long` | 否 | 请求的视口宽度，单位 CSS 像素。默认 `0`，由浏览器决定。过大或过小的值浏览器可能会收紧。 |
| `height` | `unsigned long long` | 否 | 请求的视口高度，规则与 `width` 相同。只设 `width` 与 `height` 其中之一会抛 `RangeError`。 |
| `disallowReturnToOpener` | `boolean` | 否 | 默认 `false`。为 `true` 时提示浏览器不要在窗口上显示「返回标签页」按钮。Chrome 124。 |
| `preferInitialWindowPlacement` | `boolean` | 否 | 默认 `false`。为 `true` 时提示浏览器使用请求的尺寸与默认位置，而不是恢复上一个已关闭画中画窗口的尺寸和位置。Chrome 130。 |

两个布尔成员都只是提示；规范用的是 should 与 may，两者都不遵守的浏览器也符合规范。

## 异常

`requestWindow()` 在下列情况下拒绝其 Promise，顺序即规范的检查顺序。

| 异常 | 条件 |
|---|---|
| `NotSupportedError` | 浏览器的 Document Picture-in-Picture 支持标志为 `false`（例如被策略禁用）。 |
| `NotAllowedError` | 调用方窗口不是顶层可遍历对象（iframe）；或调用方本身就是画中画窗口；或窗口没有瞬时用户激活。 |
| `RangeError` | `width` 大于零但 `height` 缺失或为零，或反过来。 |

在同一标签页再开一个窗口不算错误：规范先关闭已有的画中画窗口，再以新窗口兑现。

:::observed
在 Chrome 中，用户激活处理函数之外调用 `requestWindow()` 会以 `NotAllowedError: Document PiP requires user activation` 拒绝；只传 `{ width: 400 }` 会以 `RangeError: Height must be specified if width is specified` 拒绝；从 iframe 调用会以 `NotAllowedError: Opening a PiP window is only allowed from a top-level browsing context` 拒绝。这些字符串来自 Chromium 的 [`picture_in_picture_controller_impl.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/document_picture_in_picture/picture_in_picture_controller_impl.cc) 与 [`document_picture_in_picture.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/document_picture_in_picture/document_picture_in_picture.cc)（chromium.googlesource.com）。
:::

## 示例

每个示例都先检查 `"documentPictureInPicture" in window`，检查不通过时内容留在主文档中。`requestWindow()` 的调用留在 `click` 处理函数内，因为它会消耗瞬时激活。

### 把播放器连同样式表弹出，关闭时再放回原位

新窗口一开始是空的，所以先复制打开方的样式表，再把节点移过去。监听画中画窗口的 `pagehide`，用户关闭窗口时把播放器放回原来的容器。

```js
const mainContainer = document.querySelector("#player-slot");
const player = document.querySelector("#player");
const popOut = document.querySelector("#pop-out");

if (!("documentPictureInPicture" in window)) {
  popOut.hidden = true; // 该浏览器没有弹出能力，播放器留在页面内
}

popOut.addEventListener("click", async () => {
  const pipWindow = await documentPictureInPicture.requestWindow({
    width: 320,
    height: 180,
  });

  for (const sheet of document.styleSheets) {
    try {
      const css = [...sheet.cssRules].map((rule) => rule.cssText).join("");
      const style = document.createElement("style");
      style.textContent = css;
      pipWindow.document.head.append(style);
    } catch {
      const link = document.createElement("link");
      link.rel = "stylesheet";
      link.href = sheet.href; // 跨源样式表读不到规则，改为按 URL 链接
      pipWindow.document.head.append(link);
    }
  }

  pipWindow.document.body.append(player);
  pipWindow.addEventListener("pagehide", () => mainContainer.append(player));
});
```

读取跨源样式表的 `cssRules` 会抛 `SecurityError`，所以 `catch` 分支改为按 URL 链接该样式表。

### 复用已打开的窗口而不是再开一个

`documentPictureInPicture.window` 能告诉你是否已有窗口打开。复用它可以避免规范要求的「先关再开」带来的闪烁。

```js
async function getPipWindow(button) {
  if (!("documentPictureInPicture" in window)) return null;

  const existing = documentPictureInPicture.window;
  if (existing) return existing;

  try {
    return await documentPictureInPicture.requestWindow({
      disallowReturnToOpener: true,
    });
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
    return null;
  }
}

button.addEventListener("click", async () => {
  const pipWindow = await getPipWindow(button);
  if (!pipWindow) {
    button.textContent = "无法弹出";
    return;
  }
  pipWindow.document.body.textContent = `打开于 ${new Date().toLocaleTimeString()}`;
});
```

辅助函数返回 `null`，调用方就能渲染页内状态，而不是留下一个未处理的拒绝。

## 另请参阅

- [Media Session API](/zh/reference/capabilities/media-session/)，其 `enterpictureinpicture` 动作处理函数是不用点击也能打开该窗口的唯一途径
- [Window Management API](/zh/reference/capabilities/window-management/)，用于把普通窗口放到多块屏幕上
- [Screen Wake Lock API](/zh/reference/capabilities/wake-lock/)，浮动播放器运行时保持屏幕常亮
- [Document Picture-in-Picture: requestWindow() method](https://wicg.github.io/document-picture-in-picture/#dom-documentpictureinpicture-requestwindow)（wicg.github.io）
- [Document Picture-in-Picture API](https://developer.chrome.com/docs/web-platform/document-picture-in-picture)（developer.chrome.com）