跳转到内容

能力 · API

Document Picture-in-Picture API

发布于

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)。

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 缺失或为零,或反过来。

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

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

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

Section titled “把播放器连同样式表弹出,关闭时再放回原位”

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

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 链接该样式表。

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

Section titled “复用已打开的窗口而不是再开一个”

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

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,调用方就能渲染页内状态,而不是留下一个未处理的拒绝。

规范

规范状态
Document Picture-in-Picture: requestWindow() methodWICG 草案
Document Picture-in-Picture: DocumentPictureInPictureOptions dictionaryWICG 草案