跳转到内容

Document Picture-in-Picture:置顶窗口

发布于

一句话: 根据 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 的选项对象:

// 创建将要移入画中画窗口的内容,以及它停靠在主文档中时的容器。
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 做特性检测; 不支持时回退为空操作——上面的点击处理函数已经这样做了防护,但该检测也可以单独复用在应用 需要预先判断弹出功能是否可用的任何地方(例如完全隐藏“弹出播放器”按钮):

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 动作处理函数。