浏览器中的画中画(Picture-in-Picture)模式及其 API

5,704 阅读6分钟
原文链接: zhuanlan.zhihu.com

想边刷微博边追剧?想边聊微信边看球赛?从浏览器支持扩展功能开始,人们就一直有这样的需求:

Chrome看视频,有没有可以弹出视频窗口的插件?

各个国产壳子浏览器也争相内置“视频弹窗播放”的功能,比如当年的火狐中国版

火狐魔镜

这么多年过去了,正统浏览器们终于要内置这个功能了,而且还有配套的 API 给开发者使用。

WICG,全名为 Web Incubator Community Group(Web 孵化器社区小组),主要成员为 Chrome 的工程师们,他们从一年前开始标准化这个能让视频弹窗播放的特性,起名为 Picture-in-Picture(画中画),缩写为 PiP。

Chrome 从今年年初开始着手实现,到本文撰稿时,已经实现的差不多了。这篇文章将主要讲两方面:Chrome 当前实现的 PiP 交互是怎么样的,以及有哪些 API 让我们开发者使用。

Chrome 中的 PiP 交互

Chrome 官方提供了一个 demo 页面 Picture-in-Picture Sample,我用这个 demo 录了一个简单的演示视频:

从视频中可以看到,Chrome 在原生的 video 控件中提供了开启画中画模式的菜单项,一旦开启画中画模式,原本页面中内联(inline)展现的那个 video 还占据着原来的位置,各种控件也都在,但最重要的视频画面已经被无缝的转移到了新的弹出窗口中了,只剩下一张 poster 图片和一层灰色的遮罩。

我们把弹出的那个视频窗口叫做 PiP 窗口,PiP 窗口只有视频画面,没有标题栏和地址栏(chrome-less),这一点和用 window.open()打开的弹窗不一样。同时,PiP 窗口也没有完整的视频控件,只有开始/暂停/关闭按钮三个,时间、进度条、音量这些控件都没有。PiP 窗口还支持拖拽改变大小(不能大于一个象限),以及支持拖拽到任意位置(录屏里没有体现,因为在本文撰写时还不支持)。还有最重要的一点是,PiP 窗口在所有窗口中是置顶的(always on top),正如我在录屏里演示的,当焦点切换到 Safari 后,Chrome 中打开的 PiP 窗口仍然是在最上层的。

另外一个没有在录屏里体现出来的交互是,“同一时刻只能有一个 PiP 窗口”。同一个页面里如果为多个视频开启画中画模式,前面开启的那个会自动退出画中画模式,同一个浏览器窗口下的多个 Tab 里的视频亦如此,同一个浏览器打开的多个浏览器窗口亦如此(在本文撰稿时,Chrome 还没有实现最后一种情况,即多个浏览器窗口可以同时开启多个 PiP 窗口,已确认是 bug 不是 feature)。

PiP 相关 API

1. video 元素新增的方法 requestPictureInPicture()

请求让该 video 元素进入画中画模式,返回一个 promise,如果没有异常,这个 promise 包的值会是一个 PictureInPictureWindow对象,这个对象就代表弹出的那个 PiP 窗口,后面会单独讲它的 API。

async function openPiP(video) {
  try {
    const pipWindow = await video.requestPictureInPicture() // 进入画中画模式
    ...
  } catch (e) {
    console.error(e) // 处理异常
  }
})

哪些情况下进入画中画模式会失败?一共有 5 种情况:

  1. 操作系统不支持、或者用户通过浏览器选项禁用了此功能,此时 document.pictureInPictureEnabled 属性会返回 false
  2. 视频文件错误、或者没有视频流只有音频流
  3. 此次请求不是由用户操作触发的,比如用户没有点击任何按钮,页面自动执行该方法,会被当做恶意行为拦截掉
  4. 当前页面通过 feature-policy 禁用了画中画特性,此时 document.pictureInPictureEnabled属性也会返回 false
  5. 当前 video 元素通过 disablePictureInPicture 属性(HTML 属性和 DOM 属性均可)禁用了画中画特性

2. video 元素新增的属性 disablePictureInPicture

通过该属性可以禁用 video 元素的画中画特性,右键菜单中的“画中画”选项会被禁用。

通过 HTML 属性:

<video src="..." disablePictureInPicture>

通过 DOM 属性:

video.disablePictureInPicture = true

3. video 元素新增的事件 enterpictureinpictureleavepictureinpicture

video.addEventListener('enterpictureinpicture', function(pipWindow) {
  // 进入了画中画模式,可以拿到 pipWindow 对象
})

video.addEventListener('leavepictureinpicture', function() {
  // 退出了画中画模式
})

4. document 上新增的方法 exitPictureInPicture()

因为一个页面只能打开一个 PiP 窗口,所以让 video 元素退出画中画模式的方法不在 video 元素自己身上,而在 document 上。

这个方法也返回一个 promise,不过 promise 包的值是个 undefined

5. document 上新增的属性 pictureInPictureElementpictureInPictureEnabled

类似于document.pointerLockElementdocument.fullscreenElementdocument.pictureInPictureElement 会返回当前页面中处于画中画模式的 video 元素,如果没有的话,返回 null

document.pictureInPictureEnabled上面已经提到过了,在当前页面不支持或被禁用画中画模式的情况下会返回 false,否则返回 true

这两个属性都是只读的。

6. PictureInPictureWindow对象的 API

requestPictureInPicture()方法的返回值和 enterpictureinpicture事件的回调参数中可以拿到 pipWindow 对象,该对象有两个属性 widthheight,还支持一个resize事件,在用户改变 PiP 窗口大小时会触发。

async function openPiP(video) {
  const pipWindow = await video.requestPictureInPicture()
  console.log(pipWindow.width, pipWindow.height) // 打印了默认的窗口大小

  pipWindow.addEventListener('resize', function() {
    console.log(pipWindow.width, pipWindow.height) // 用户改变 PiP 窗口大小时触发
  })
}

注意这里的 widthheight是只读的,你不能通过给他们赋值来改变窗口大小。

杂项

1. Safari 中已有的画中画 API

Safari 在两年前就支持了画中画模式,还有一套带前缀的配套 API,叫 Presentation Mode。好消息是这个新规范也有 Safari 的人参与了制定,事实上目前为止四大浏览器厂商只剩 Edge 还没表示支持。

2. 画中画这个名字的由来

“画中画”这东西是十几年前电视机上的一个功能,我自己去年看到这个规范的时候也觉的它这个名字比较误导人,现在规范 issue 里也有个人在问,希望你没把它理解成是嵌套的 <img>

3. Chrome 哪个版本支持

现在的 Chrome Canary(69)还没有默认打开这个特性,需要你手动开启

chrome://flags/#enable-experimental-web-platform-features

chrome://flags/#disable-background-video-track

chrome://flags/#enable-picture-in-picture

这三个开关,不用 Canary 的同学老实等两个月。

4. 可不可能弹出 video 以外的元素

规范制定者表示未来有可能