# Document Picture-in-Picture：置顶窗口

> Document Picture-in-Picture API 如何打开置顶 HTML 窗口，其 requestWindow() 选项与运行时检测，以及 Chrome、Firefox 支持情况。

**一句话：** 根据 MDN 的说明，"Document Picture-in-Picture API 使得打开一个可以承载任意
HTML 内容的置顶窗口成为可能"，它扩展了此前只能承载单个 `<video>` 元素的画中画 API。

## 这是什么

MDN 将 Document Picture-in-Picture 窗口描述为与 `Window.open()` 打开的同源窗口类似，但有
几点不同："浮动于其他窗口之上"、"生命周期不会超出打开它的窗口"、"不能被导航"，并且"网站
无法设置该画中画窗口的位置"。MDN 还指出该窗口"同一时刻每个浏览器标签页最多只能有一个"。
MDN 列举的用例包括始终置顶的自定义视频播放器、视频会议控制面板，以及计时器、待办事项等
始终可见的效率工具。

## 支持情况

根据 MDN 的 browser-compat-data，`DocumentPictureInPicture` 接口及其 `requestWindow()`
方法自 Chrome 116 起受支持（Edge 与 Chrome 保持一致）、自 Firefox 151 起受支持。同一份
数据将该特性记录为 Safari 不支持，且 Chrome for Android 与 Firefox for Android 也均不
支持（两者的 `version_added` 均为 `false`）。

## 如何使用

根据 MDN，入口是 `Window.documentPictureInPicture`，通过调用
`DocumentPictureInPicture.requestWindow()` 打开该窗口，该方法"返回一个 Promise，
resolve 为窗口自身的 Window 对象"。根据 `requestWindow()` 参考文档，该调用"需要瞬态
激活（transient activation）"，因此必须在用户手势处理函数（例如点击监听器）内部运行，
并且可以传入包含 `width`、`height`、`disallowReturnToOpener`、
`preferInitialWindowPlacement` 的选项对象：

```js
// 创建将要移入画中画窗口的内容，以及它停靠在主文档中时的容器。
const mainContainer = document.createElement("div");
const playerContainer = document.createElement("div");
playerContainer.textContent = "正在播放…";
mainContainer.append(playerContainer);
document.body.append(mainContainer);

const button = document.createElement("button");
button.textContent = "弹出播放器";
document.body.append(button);

button.addEventListener("click", async () => {
  if (!("documentPictureInPicture" in window)) {
    // 不支持 Document Picture-in-Picture——让播放器留在主文档中，而不是抛出异常。
    return;
  }

  const pipWindow = await documentPictureInPicture.requestWindow({
    width: 320,
    height: 180,
  });

  // 复制应用的样式表，让移动过去的内容保持相同样式。
  [...document.styleSheets].forEach((sheet) => {
    try {
      const cssRules = [...sheet.cssRules].map((rule) => rule.cssText).join("");
      const style = document.createElement("style");
      style.textContent = cssRules;
      pipWindow.document.head.appendChild(style);
    } catch (e) {
      // 跳过任何无法读取的样式表。
    }
  });

  pipWindow.document.body.append(playerContainer);

  pipWindow.addEventListener("pagehide", () => {
    // 画中画窗口关闭时，把内容移回主窗口。
    mainContainer.append(playerContainer);
  });
});
```

## 运行时检测与回退

在调用 `requestWindow()` 之前，先对 `window` 上的 `documentPictureInPicture` 做特性检测；
不支持时回退为空操作——上面的点击处理函数已经这样做了防护，但该检测也可以单独复用在应用
需要预先判断弹出功能是否可用的任何地方（例如完全隐藏"弹出播放器"按钮）：

```js
function supportsDocumentPictureInPicture() {
  return "documentPictureInPicture" in window;
}

if (!supportsDocumentPictureInPicture()) {
  // 不支持 Document Picture-in-Picture——隐藏任何提供弹出功能的界面，
  // 让内容始终留在主文档中。
  document.querySelectorAll("[data-requires-document-pip]").forEach((el) => {
    el.hidden = true;
  });
}
```

## 实用清单

- 根据 MDN 的 `requestWindow()` 参考文档，该方法需要瞬态激活——只能在点击等用户手势处理
  函数内部调用。
- 根据 MDN，画中画窗口"生命周期不会超出打开它的窗口"且"不能被导航"——不要把它当作独立、
  长期存在的窗口来依赖。
- 样式表不会自动复制；需要向画中画窗口自身的 `document.head` 追加 `<style>` 或 `<link>`
  元素，移动过去的内容才能正确渲染。
- MDN 的 browser-compat-data 显示该特性在 Chrome for Android、Firefox for Android 以及
  Safari 上均不支持，因此应在所有平台上都做特性检测，而不要只假设桌面端存在差异。
- 根据 MDN，`enter` 事件只在你自己的代码以编程方式打开窗口时触发；当浏览器自身（例如
  因为标签页被切换）把内容移入该窗口时，这个事件不会触发——此时应改用类型为
  `enterpictureinpicture` 的 `MediaSession` 动作处理函数。

## 相关参考

- [Screen Wake Lock API](/zh/reference/capabilities/wake-lock/)
- [WebRTC](/zh/reference/capabilities/webrtc/)