尝试 Chrome 的新画中画(Picture in Picture with dom)功能

开篇废话

之前因为川普〇击案的新闻然后打开了 TikT〇k 。
突然看到弹窗了新功能 Floating Player

toktik
在点击试用后的我马上意识到现在的浏览器可以画中画里面可以插 HTML 了。

于是在想如何给自己的项目引入。

首先参考 Chrome developer 的文章查看大概原理,然后再来看我的实践:

正文

浏览器适配度

首先泼一盆冷水:
caniuse.com 来看,只有 Chromium 116+ 以上的版本支持。
并且 Firefox / Safari 目前还是不支持的。
因此目前只能当作实验特性,一定要用备用方案提供才行,否则用户将无法正常交互。

使用场景

画中画使用场景基本上是给播放器使用的。
非播放器的画中画目前只观察到 Android 里面的 HttpCanary 在抓包时候的 UI 了。
本文中将介绍我个人项目的一个使用场景并且展示如何将其引入。

原来的逻辑

我选中了一个项目的打开日志中心的功能来进行修改,之前的方案是 window.open 以 popup(弹窗)的方式打开个新网页,大概代码是这个样子的:

window.open(
    "/logViewer.php",
    "_status_1",
    "width=1024,height=768,status=no,location=no,toolbar=no,menubar=no,scrollbars=no"
);

此网页,我希望用户可以同时打开多个日志页面并且不被干扰(原来页面也经常用得到,直接 _blank 打开又要切换回去)
因此最后使用 popup 弹窗来打开的方案,网页内弄个全屏 modal 或者新开个正常的标签页都被我否决了。

实现

触发PIP

首先可以知道是 documentPictureInPicture 的功能实现,于是我们的代码逻辑应该是这个样子:

if ('documentPictureInPicture' in window && !navigator.userAgent.includes('Android') && !navigator.userAgent.includes('Mobile')) {
    const pipWindow = await documentPictureInPicture.requestWindow({
        width:1024,
        height:768,
    });
} else {
    window.open(
        "/logViewer.php",
        "_status_1",
        "width=1024,height=768,status=no,location=no,toolbar=no,menubar=no,scrollbars=no"
    );
}

在这个逻辑里,如果我们判断是 PC 设备(在移动端 PIP 感觉意义不明)然后浏览器支持 documentPictureInPicture 则启用,否则为 fallback 方案(window.open):

pip window

可以看到在右下角弹了个 pip 窗口,不过画面是白的(毕竟我们还没开始给窗口写东西)
F12 F12 可以看到,location.href = about:blank 不过不用担心,请求的时候还是同域的,可以放心传曲奇之类的。

写入内容

一开始我想的是,直接 location.href = xxx 不就可以了,于是我试了一下:

pipWindow.location.href = '/logViewer.php';

然后 PIP 窗口就消失了。

Chrome developer 的教程是教你移动网页现有的元素到 DOM 窗口。
在下面的例子中,本处元素除了样式以外会被继承到 PIP 里面。
点击这几行的文字会弹窗,在 PIP 里面点击也会弹窗,但是文字从红色变成了黑色,也就是 css 样式默认不会继承。
然后点击「运行」后,此处描述文本将会移动到 PIP 窗口里面。

const example1 = document.querySelector("#example1");
const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.append(example1);
// 使用 CloneNode 将不会继承事件
// pipWindow.document.body.append(example1.cloneNode(true););

复制样式看个人喜好,如果懒的话窗口内直接再引用和主界面一样的 CSS 文件就可以,也可以先拷贝样式,参考官网教程即可,这里不再演示。 因此没有什么办法快速将 popup 弹窗改成 PIP
了吗?

在思考了一阵后,还是有的。
我们可以在PIP里面塞个 iframe 来实现最少的代码改动。
不过感觉是在套娃了,并且要求 iframe 里面的窗口是同域,或者用些奇怪的办法过 CORS (比如说鉴权部分用别的逻辑来处理)

最后代码变成了这个样子:

const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.innerHTML =  `<style>body{margin:0;} iframe{border:0;width:100%;height:100%;}</style><iframe src="/"></iframe>`

sandbox 之类的策略请参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe

一下子迁移工作量就少了很多呢~

交互

这部分内容相信看 Chrome developer 的教程更详细,无非是把 document 变成了 pipWindow.document (以此类推)

比如这是检测网页被导航至新的 url 的检测例子:

const pipWindow = await documentPictureInPicture.requestWindow();
pipWindow.document.body.innerHTML =  `<style>body{margin:0;} iframe{border:0;width:100%;height:100%;}</style><iframe src="/"></iframe>`
pipWindow.document.querySelector('iframe').addEventListener('load', ()=>{
    console.log('网页被导航至新 url');
});
// 当然也可以继续套娃,在 iframe 插点奇奇怪怪的东西: 
pipWindow.document.querySelector('iframe').contentWindow.addEventListener('beforeunload', function(event) {
    event.preventDefault();
    event.returnValue = '';
});

其他交互看按照需求来做,总的来说比 popup 方便一点。
popup 只能用 postmessage 之类的来交互,写的好累啊(

总结

最终我放弃了 PIP 方案,因为有一些局限性:

  • PIP 是独占模式,即最多只有 1 个 PIP 窗口,这对于我有同时打开多个窗口的需求还是差了点意思。
  • 网页被关闭后PIP窗口也会被关闭,在播放器里面是非常合理的,不过在我的需求里感觉不太行
  • 始终为在右下角打开,不能先指定位置,如果用户正好没注意到右下角可能会有疑惑
  • 窗口有最大限制,只能 3/4 左右大小(能基本占满屏幕的话就成为新一代牛皮癣了)