1. 简介
本节为非规范性内容。
目前已有一个 Web API 能将 HTMLVideoElement
放入画中画窗口(requestPictureInPicture())。这限制了网站定制画中画体验(PiP)的能力。我们希望通过提供始终置顶窗口里的完整 Document
来扩展这一功能。
这个新窗口类似于通过现有 open() 方法在 Window
上打开的同源空白窗口,但有一些细微区别:
-
PiP 窗口会浮在其他窗口之上。
-
PiP 窗口不会比打开它的窗口存活更长。
-
网站无法设置 PiP 窗口的位置。
-
PiP 窗口不可导航(任何改变到新文档的 `window.history` 或 `window.location` 调用都会关闭 PiP 窗口)。
2. 依赖项
本规范中的 IDL 片段必须按照 Web IDL 规范对符合规范的 IDL 片段的要求进行解释。[WEBIDL]
3. 安全注意事项
3.1. 安全上下文
此 API 仅限于 [SECURE-CONTEXTS]。
3.2. 欺骗
要求用户代理在 DocumentPictureInPicture
窗口上提供足够的界面,防止恶意网站利用浮窗功能欺骗其它网站或系统界面。
3.2.1. 定位
用户代理必须阻止网站设置窗口位置,以避免网站故意将窗口定位到可能让用户误以为属于别页界面的地方。特别是 moveTo()
和 moveBy()
API 必须在文档画中画窗口中禁用。
3.2.2. 来源可见性
要求用户代理始终向用户明确显示正在控制 DocumentPictureInPicture
窗口的来源,确保用户知道内容来自哪里。例如,用户代理可以在窗口标题栏显示网站的来源。
3.2.3. 最大尺寸
用户代理应限制文档画中画窗口的最大尺寸,防止网站用始终置顶窗口遮盖整个屏幕把用户锁定在画中画窗口里。这也有助于防止欺骗用户桌面。
3.2.4. 全屏
用户代理必须出于与最大尺寸限制相同的理由,防止文档画中画窗口进入全屏模式。具体而言,这意味着 requestFullscreen()
API 必须对文档画中画窗口中的所有元素禁用。
3.3. IFrame 内联框架
本 API 仅适用于 顶层可遍历对象。但 DocumentPictureInPicture
Window
仍可包含 HTMLIFrameElement,包括
跨域 HTMLIFrameElement。
4. 隐私注意事项
4.1. 指纹识别
当 PiP 窗口关闭再重新打开时,用户代理可重复之前窗口的大小和位置,以优化用户体验。但建议用户代理不要跨来源复用大小或位置,否则可能被恶意网站用于识别用户。
5. API
[Exposed =Window ]partial interface Window { [SameObject ,SecureContext ]readonly attribute DocumentPictureInPicture documentPictureInPicture ; }; [Exposed =Window ,SecureContext ]interface :DocumentPictureInPicture EventTarget { [NewObject ]Promise <Window >requestWindow (optional DocumentPictureInPictureOptions = {});options readonly attribute Window window ;attribute EventHandler ; };onenter dictionary { [DocumentPictureInPictureOptions EnforceRange ]unsigned long long = 0; [width EnforceRange ]unsigned long long = 0;height boolean =disallowReturnToOpener false ;boolean =preferInitialWindowPlacement false ; }; [Exposed =Window ,SecureContext ]interface :DocumentPictureInPictureEvent Event {(constructor DOMString ,type DocumentPictureInPictureEventInit ); [eventInitDict SameObject ]readonly attribute Window ; };window dictionary :DocumentPictureInPictureEventInit EventInit {required Window ; };window
DocumentPictureInPicture
对象允许网站创建并打开一个新始终置顶的 Window
,并监听与该 Window
打开与关闭相关的事件。
每个 Window
对象都有一个相关的 documentPictureInPicture API,
它是和 DocumentPictureInPicture
实例一起创建于 Window
的。
documentPictureInPicture 的 getter 步骤如下:
每个 DocumentPictureInPicture
对象都有一个相关的
last-opened window
,即最初为 null,通过
requestWindow() 方法步骤设置的
Window
对象。
window 的 getter 步骤如下:
-
令 win 为 this 的 last-opened window。
-
如果 win 不为
null且 win 的 closed 属性为false,则返回 win。 -
返回
null。
requestWindow(options) 方法步骤如下:
-
如果 Document Picture-in-Picture 支持 为
false,则抛出 "NotSupportedError"DOMException。 -
如果 this 的 相关全局对象 的 navigable 不属于 顶级 traversable,则抛出 "
NotAllowedError"DOMException。 -
如果 this 的 相关全局对象 的 navigable 的 Is Document Picture-in-Picture 布尔值为
true,则抛出 "NotAllowedError"DOMException。 -
如果 this 的 相关全局对象 没有 瞬时激活,则抛出 "
NotAllowedError"DOMException。 -
如果 options["
width"] 存在且大于0,但 options["height"] 不存在或为0,则抛出RangeError。 -
如果 options["
height"] 存在且大于0,但 options["width"] 不存在或为0,则抛出RangeError。 -
令 win 为 this 的 last-opened window。若 win 不为
null且 win 的 closed 属性为false,则 关闭 win 的 navigable。 -
用户代理可选地 关闭所有现有画中画窗口。
-
令 pip traversable 为 创建新的顶级 traversable 的结果,参数为 this 的 相关全局对象 的 navigable 的 活动浏览上下文 和 "
_blank"。
结果中 Document
的 URL 将是 `about:blank`,但其
文档基础 URL 将回退为调用
requestWindow()
的发起方的基础 URL。部分浏览器对普通 `about:blank` 弹窗并未实现该行为;讨论参见
whatwg/html#421。建议实现者确保画中画文档窗口
实现此继承,以避免进一步的兼容性问题。
-
将 pip traversable 的 活动文档的 mode 设为 this 的 相关全局对象的 关联 Document 的 mode。
-
将 pip traversable 的 Is Document Picture-in-Picture 布尔值设为
true。 -
如果 options["
width"] 存在且大于零: -
如果 options["
height"] 存在且大于零:
如果 options["preferInitialWindowPlacement"]
存在且为 true,则用户代理可用此提示优先采用第 13、14 步类似行为,而不是考虑之前已关闭 pip traversable 窗口的位置或大小。
-
如果 options["
disallowReturnToOpener"] 存在且为true,则用户代理不应在画中画窗口显示返回开启页面的 UI 控件。
对于视频和文档画中画,用户代理通常会显示一个
按钮,供用户返回原始页面并关闭
画中画窗口。虽然此操作在大多数情况下是合理的
(尤其是对于视频画中画窗口,它会将视频返回到
主文档中),但对于文档画中画
窗口而言并不总是合理。disallowReturnToOpener
是网站向
用户代理提供的一个提示,用于表明该操作对于其
特定的文档画中画体验是否合理。
-
将 pip traversable 的 活动浏览上下文窗口配置为浮动于其他窗口之上。
-
将 this 的 last-opened window 设为 pip traversable 的 活动窗口。
-
队列一个全局任务, 任务源为 DOM manipulation task source, 参数为 this 的 相关全局对象,以 触发一个事件, 事件名为
enter, 事件类型为DocumentPictureInPictureEvent, 事件对象window属性初始化为 pip traversable 的 活动窗口。
虽然窗口的尺寸可由网站配置,但初始位置由用户代理自行决定。
enter-
当 PiP 窗口被打开时,在
DocumentPictureInPicture上触发。
6. 概念
6.1. 文档画中画支持
每个用户代理都拥有一个 文档画中画支持 布尔值,其值为 实现自定义(并且可能会根据用户偏好而变化)。
6.2. DocumentPictureInPicture 窗口
每个顶级 traversable都有一个 是否为文档画中画
布尔值,其默认值为 false,但可以在 requestWindow() 方法步骤中被设置为
true。
用户代理通常会暂停最小化或对用户不可见窗口的渲染,并限制脚本执行。当有一个文档画中画窗口打开时,开启者窗口仍可能执行影响画中画窗口内容的脚本。
建议实现者考虑对这类开启者窗口限流的影响,也建议开发者尽量在画中画窗口自身执行应用逻辑。
6.3. 关闭文档画中画窗口
达成足够共识后,将其合并进 definitely close。
修改 definitely close 的第 2 步, 即“If the result of checking if unloading is user-canceled for toUnload is not "continue", then return.”,修改为:
-
若 traversable 的 是否为文档画中画布尔值为 true, 则跳过此步。否则,若对 toUnload 检测卸载是否被用户取消 的结果不是 "continue",则返回。
6.4. 关闭现有画中画窗口
关闭现有画中画窗口的操作:
-
对于用户代理的 顶层可遍历对象集合 中每一个 top-level traversable:
-
如果 top-level traversable 的 是否为文档画中画 布尔值为
true,则 关闭 top-level traversable。 -
如果 top-level traversable 的 活动文档 的 pictureInPictureElement 不为
null,则以 top-level traversable 的 活动文档 运行 退出画中画算法。 -
对 top-level traversable 的 活动文档 的每个 navigable 后代 navigables:
-
如果 navigable 的 活动文档 的 pictureInPictureElement 不为
null,则以 navigable 的 活动文档 运行 退出画中画算法。
-
-
6.5. 单一画中画窗口
任意 顶层可遍历对象 在同一时刻最多只能打开一个文档画中画窗口。如果 顶层可遍历对象 的 活动窗口的 documentPictureInPicture API 的 最后打开的窗口非
null,且尝试再打开新的文档画中画窗口,用户代理应按照 requestWindow() 方法步骤关闭现有 最后打开的窗口。
但是否所有 顶层可遍历对象画中画窗口只能有一个,取决于具体实现与平台。因此,当已存在画中画请求,而仍有 顶层可遍历对象的 是否为文档画中画布尔值为 true 或 活动文档 的 pictureInPictureElement
不为 null 时的行为,留作实现细节:用户代理可 关闭现有画中画窗口,也可允许创建多个画中画窗口。
6.6. 原始文档或 PiP 文档销毁时关闭 PiP 窗口
给定 Document
document,关闭所有关联的文档画中画窗口操作:
-
令 navigable 为 document 的 node navigable。
-
如果 navigable 不是 顶层可遍历对象,则中止步骤。
-
令 win 为 navigable 的 活动窗口的 documentPictureInPicture API 的 最后打开的窗口。
-
如果 win 不为
null且 win 的 closed 属性为false,则 关闭 win 的 navigable。
等获得足够共识后并入 destroy 。
向 destroy 末尾添加第 10 步:
-
关闭所有关联的文档画中画窗口,参数为 document。
这确保当具有打开的文档画中画窗口的页面关闭时,其 PiP 窗口也会关闭。
6.7. 原始文档或 PiP 文档导航时关闭 PiP 窗口
达成足够共识后,将其合并进 navigate。
修改 navigate 的第 23.3 步, “Queue a global task on the navigation and traversal task source given navigable’s active window to abort a document and its descandants given navigable’s active document.”, 并在其后立刻插入第 23.4 步:
-
在 navigation and traversal task source 上为 navigable 的 活动窗口 排队一个全局任务以中止 给定 navigable 的 活动文档 及 关闭所有关联的文档画中画窗口 ,参数同 navigable 的 活动文档。
-
如果 navigable 是一个 顶级 traversable,其 是否为文档画中画布尔值为
true,则中止这些步骤。
这样可确保当含有打开的文档画中画窗口的页面发生导航时,其画中画窗口也会被关闭。同时也确保当文档画中画窗口内的文档发生导航时,该窗口本身也会被关闭。
6.8. 调整 PiP 窗口大小
虽然以编程方式调整文档画中画窗口大小非常有用,但由于该窗口始终置顶,因此无约束调整大小可能被滥用、造成干扰或侵扰。为缓解此类问题,且不完全禁用窗口调整大小 API,我们会让这些 API 在文档画中画窗口消耗用户手势。
等获得足够共识后并入 resizeTo()。
在 resizeTo()
第 3 步之后增加新步骤:"如果 target 不是脚本创建(而不是用户行为)的 辅助浏览上下文,则返回。":
-
如果 target 的 顶层可遍历对象 的 是否为文档画中画 布尔值为
true,则:-
如果 this 的 相关全局对象没有 临时激活,抛出 "
NotAllowedError"DOMException。
-
等获得足够共识后并入 resizeBy()。
在 resizeBy()
第 3 步之后增加新步骤:"如果 target 不是脚本创建(而不是用户行为)的 辅助浏览上下文,则返回。":
-
如果 target 的 顶层可遍历对象 的 是否为文档画中画 布尔值为
true,则:-
如果 this 的 相关全局对象没有 临时激活,抛出 "
NotAllowedError"DOMException。
-
6.9. 移动 PiP 窗口
为防止在 § 3.2.1 定位 中所述的欺骗,我们将对文档画中画窗口禁用 moveTo()
和 moveBy()。
达成足够共识后,将其合并到 moveTo()
和 moveBy()
的步骤中。
在 moveTo()
第 3 步之后新增一步,“If target is not an
auxiliary browsing context that was created by a script (as
opposed to by an
action of the user), then return.”:
-
如果 target 的 顶级 traversable 的 是否为文档画中画 布尔值为
true,则返回。
在 moveBy()
第 3 步之后新增一步,“If target is not an
auxiliary browsing context that was created by a script (as
opposed to by an
action of the user), then return.”:
-
如果 target 的 顶级 traversable 的 是否为文档画中画 布尔值为
true,则返回。
6.10. 聚焦开启者窗口
画中画窗口能够重新聚焦其开启的标签页通常很有用,例如当较小的窗口形态无法满足用户所需体验时。我们修改 focus()
API,使其在画中画窗口聚焦其
开启者时可以获取系统层面的焦点。
达成足够共识后,将其合并到 focus()
中。
在 focus()
第 4 步之后新增一步,“Run the focusing steps with current.”:
-
如果 current 是一个 顶级 traversable,则:
-
令 pipWindow 为 current 的 活动窗口 的 documentPictureInPicture API 的 last-opened window。
-
将系统焦点赋予开启者并不一定需要关闭文档画中画窗口。如果网站希望在聚焦后关闭文档画中画窗口,始终可以在
文档画中画窗口自身上调用 close()
来实现。
6.11. CSS display-mode
CSS 显示模式媒体特性 picture-in-picture 允许 Web 开发者编写仅在(部分) Web 应用以画中画模式显示时才生效的特定 CSS 规则。
6.12. 用户激活传播
由于文档画中画窗口的特性,窗口内按钮的事件处理程序常常最终在开启者的上下文中运行。 这会使网站调用 activation consuming APIs 变得不便,因为有时文档画中画窗口具有 瞬时激活,而开启者却没有。
为简化这一点,我们将更新 activation notification 的步骤,使在文档画中画窗口触发用户激活时,也会在开启者中触发用户激活。此外,当开启者中触发用户激活时, 我们将激活文档画中画窗口内的同源框架,类似于对同源的后代框架的激活方式。
达成足够共识后,将其合并进 activation notification 的步骤。
在 activation notification 第 4 步之后新增三步,“Extend windows with the active window of each of document’s descendant navigables, filtered to include only those navigables whose active document’s origin is same origin with document’s origin”:
-
如果 document 的 节点 navigable 的 顶级 traversable 的 是否为文档画中画 布尔值为
true,则 扩展 windows,加入 document 的 节点 navigable 的 顶级 traversable 的 活动浏览上下文 的 开启者浏览上下文 的 活动窗口。 -
令 document picture-in-picture window 为 document 的 节点 navigable 的 顶级 traversable 的 活动窗口 的 documentPictureInPicture API 的 last-opened window。
-
如果 document picture-in-picture window 不为
null,则 扩展 windows,加入 活动窗口(来自 document picture-in-picture window 的 关联文档 的 后代 navigables),仅包含其 navigable 的 活动文档 的 来源 与 document picture-in-picture window 的 关联文档 的 来源 同源的那些。
此外,我们需要确保该激活被正确消费,以免被重复使用(一次用于开启者,一次用于画中画窗口)。 我们将通过在 consume user activation 中新增步骤来实现: 当消费画中画窗口的用户激活时,也消费开启者的用户激活;当消费开启者的用户激活时,也消费其关联的 画中画窗口的用户激活。
达成足够共识后,将其合并到 consume user activation 的步骤中。
在 consume user activation 第 3 步之后新增三步,“Let navigables be the inclusive descendant navigables of top’s active document.”:
-
如果 top 的 是否为文档画中画 布尔值为
true,则 扩展 navigables,加入 inclusive descendant navigables(来自 top 的 活动浏览上下文 的 开启者浏览上下文 的 活动文档)。 -
令 document picture-in-picture window 为 top 的 活动窗口 的 documentPictureInPicture API 的 last-opened window。
-
如果 document picture-in-picture window 不为
null,则 扩展 navigables,加入 inclusive descendant navigables (来自 document picture-in-picture window 的 关联文档)。
6.13. 使 PiP 窗口全屏
如 § 3.2.4 全屏 所述,文档画中画窗口不允许进入全屏。我们更新 fullscreen is supported 以强制执行该限制。
达成足够共识后,将其合并进 fullscreen is supported。
将 fullscreen is supported 中 “if there is no previously-established user preference, security risk, or platform limitation.” 替换为
Fullscreen is supported 对于 Window
window 成立,当且仅当以下各项均为真:
-
不存在既有的用户偏好、安全风险或平台限制。
-
window 的 navigable 的 顶级 traversable 的 是否为文档画中画 布尔值为
false。
对 fullscreen is supported 的调用需要更新,
以传入该 Window。
7. 示例
本节为非规范性内容
7.1. 将视频播放器提取到画中画
7.1.1. HTML
< body > < div id = "player-container" > < div id = "player" > < video id = "video" src = "foo.webm" ></ video > <!-- 更多播放器元素。 --> </ div > </ div > < input type = "button" onclick = "enterPiP();" value = "进入画中画" /> </ body >
7.1.2. JavaScript
// 画中画窗口的句柄。 let pipWindow= null ; function enterPiP() { const player= document. querySelector( '#player' ); // 设置宽高,使窗口大小适配视频。 const pipOptions= { width: player. clientWidth, height: player. clientHeight, }; documentPictureInPicture. requestWindow( pipOptions). then(( pipWin) => { pipWindow= pipWin; // 剩余容器增加样式,表明播放器已在画中画内。 playerContainer. classList. add( 'pip-mode' ); // 把播放器加入画中画窗口。 pipWindow. document. body. append( player); // 监听关闭画中画事件,把视频放回主窗口。 pipWindow. addEventListener( 'pagehide' , onLeavePiP. bind( pipWindow), { once: true }); }); } // 画中画窗口关闭时调用。 function onLeavePiP() { if ( this !== pipWindow) { return ; } // 移除容器上的画中画样式。 const playerContainer= document. querySelector( '#player-container' ); playerContainer. classList. remove( 'pip-mode' ); // 把播放器放回主窗口。 const player= pipWindow. document. querySelector( '#player' ); playerContainer. append( player); pipWindow= null ; }
7.2. 访问画中画窗口的元素
const video= pipWindow. document. querySelector( '#video' ); video. loop= true ;
7.3. 监听画中画窗口事件
为优化画中画体验,网站常需定制按钮和控件以响应如点击等用户输入事件。
const pipDocument= pipWindow. document; const video= pipDocument. querySelector( '#video' ); const muteButton= pipDocument. document. createElement( 'button' ); muteButton. textContent= '切换静音' ; muteButton. addEventListener( 'click' , () => { video. muted= ! video. muted; }); pipDocument. body. append( muteButton);
7.4. 退出画中画
网站可能希望关闭 DocumentPictureInPicture
的 Window
,而不是让用户显式点击窗口的关闭按钮。他们可以通过在该 Window
对象上调用 close() 方法来实现:
// 这会关闭画中画窗口并触发已有的 onLeavePiP() 监听。 // 监听器。 pipWindow. close();
7.5. 画中画窗口关闭时取出元素
无论是网站主动关闭还是用户关闭画中画窗口时,网站常需把元素从窗口中取出。可在 pagehide
事件的处理器内实现(见上面视频播放器示例里的 onLeavePiP(),如下所示):
// 画中画窗口关闭时调用。 function onLeavePiP() { if ( this !== pipWindow) { return ; } // 去除容器上的画中画样式。 const playerContainer= document. querySelector( '#player-container' ); playerContainer. classList. remove( 'pip-mode' ); // 把播放器放回主窗口。 const player= pipWindow. document. querySelector( '#player' ); playerContainer. append( player); pipWindow= null ; }
7.6. 以编程方式调整画中画窗口大小
文档画中画窗口支持 resizeTo()
和 resizeBy()
API,但仅可在画中画窗口用户手势下使用:
const expandButton= pipWindow. document. createElement( 'button' ); expandButton. textContent= '放大画中画窗口' ; expandButton. addEventListener( 'click' , () => { // 将画中画窗口宽度增加20像素,高度增加30像素。 pipWindow. resizeBy( 20 , 30 ); }); pipWindow. document. body. append( expandButton);
7.7. 返回到打开的标签页
可用 focus()
API 从画中画窗口聚焦打开标签页(需用户手势):
const returnToTabButton= pipWindow. document. createElement( 'button' ); returnToTabButton. textContent= '返回原标签页' ; returnToTabButton. addEventListener( 'click' , () => { window. focus(); }); pipWindow. document. body. append( returnToTabButton);
7.8. CSS 画中画显示模式使用示例
下例展示如何在画中画窗口去除 body 元素边距,同时减小标题字体以更好适配画中画内内容:
@media all and( display-mode: picture-in-picture) { body{ margin : 0 ; } h1{ font-size : 0.8 em ; } }
7.9. 隐藏返回打开方按钮
虽然用户代理常在视频和文档画中画窗口显示返回打开方并关闭窗口的按钮,但对于部分网站的文档画中画体验该按钮不总是适用。可用 disallowReturnToOpener
选项隐藏按钮。
await documentPictureInPicture. requestWindow({ disallowReturnToOpener: true });
7.10. 优先初始窗口定位
文档画中画窗口打开时,用户可能手动调整大小或位置。若窗口关闭后再打开,用户代理可能用上次的位置与大小作为新窗口参考,而不是原始默认位置。
如果站点认为不应复用先前位置和大小(如为与前次无关的活动请求新画中画窗口时),可设置 preferInitialWindowPlacement
为 true 作为提示。用户代理可选择用默认位置、默认尺寸或站点提供的尺寸提示。
await documentPictureInPicture. requestWindow({ preferInitialWindowPlacement: true });
8. 致谢
感谢 Frank Liberato、Mark Foltz、Klaus Weidner、François Beaufort、Charlie Reis、Joe DeBlasio、Domenic Denicola 和 Yiren Wang 对本文档和相关讨论的意见与贡献。