使用“画中画”模式观看视频

François Beaufort
François Beaufort

借助画中画 (PiP) 功能,用户可以在浮动窗口(始终位于其他窗口之上)观看视频,以便在与其他网站或应用互动时随时关注自己正在观看的视频。

借助 Picture-in-Picture Web API,你可以为网站上的视频元素启动和控制画中画功能。欢迎查看我们的官方画中画示例

背景

2016 年 9 月,Safari 通过 macOS Sierra 中的 WebKit API 添加了对画中画的支持。6 个月后,随着 Android O 发布的使用原生 Android API 的发布,Chrome 在移动设备上自动播放了画中画视频。6 个月后,我们宣布了我们打算打造一个与 Safari 兼容的 Web API 并对其进行标准化,以便 Web 开发者能够打造和控制有关画中画的完整体验。我们到了!

了解代码

进入画中画模式

我们先简单了解视频元素以及用户与之互动的方式,例如按钮元素。

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

仅在响应用户手势时请求画中画请求,而绝不在 videoElement.play() 返回的 promise 中请求画中画。这是因为 promise 尚未传播用户手势。请改为在 pipButtonElement 上的点击处理程序中调用 requestPictureInPicture(),如下所示。您有责任处理用户点击两次后会发生的情况。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

当 promise 解析后,Chrome 会将视频缩小到一个小窗口中,以供用户四处移动并覆盖在其他窗口上。

大功告成!太棒了!您可以放下阅读,享受完美的假期。遗憾的是,情况并非总是如此。promise 可能会因以下任一原因而拒绝:

  • 系统不支持画中画功能。
  • 由于限制性的权限政策,文档无法使用画中画功能。
  • 视频元数据尚未加载 (videoElement.readyState === 0)。
  • 视频文件只包含音频。
  • 视频元素上有新的 disablePictureInPicture 属性。
  • 相应调用并非在用户手势事件处理脚本中进行(例如,点击按钮)。从 Chrome 74 开始,此功能仅适用于画中画模式下没有任何元素的情况。

下面的功能支持部分介绍了如何根据这些限制启用/停用按钮。

我们来添加一个 try...catch 代码块来捕获这些潜在错误,并让用户知道发生了什么情况。

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

无论视频元素是否处于画中画模式,其行为都相同:会触发事件,并且调用方法可以正常运行。它反映了画中画窗口中的状态变化(例如播放、暂停、跳转等),并且还可以在 JavaScript 中以编程方式更改状态。

退出画中画

现在,我们将按钮切换开关设为进入和退出画中画。我们首先必须检查只读对象 document.pictureInPictureElement 是否为视频元素。如未开启,系统会发送进入画中画模式的请求(如上所述)。否则,我们会通过调用 document.exitPictureInPicture() 请求退出,这意味着视频会重新显示在原始标签页中。请注意,此方法也会返回一个 promise。

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

收听画中画活动

操作系统通常会将画中画限制在一个窗口中,因此 Chrome 的实现会遵循此模式。这意味着用户一次只能播放一个画中画视频。您应该预料到用户会退出画中画,即使您未提出此请求也是如此。

利用新的 enterpictureinpictureleavepictureinpicture 事件处理脚本,我们可以为用户提供量身定制的体验。可以是浏览视频目录,也可以是显示直播聊天。

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

自定义画中画窗口

Chrome 74 支持画中画窗口中的播放/暂停、上一首曲目和下一首曲目按钮,您可以使用 Media Session API 控制这些按钮。

“画中画”窗口中的媒体播放控件
图 1. 画中画窗口中的媒体播放控件

默认情况下,除非视频正在播放 MediaStream 对象(例如 getUserMedia()getDisplayMedia()canvas.captureStream())或视频的 MediaSource 时长设置为 +Infinity(例如实时 Feed),否则播放/暂停按钮始终显示在画中画窗口中。为了确保播放/暂停按钮始终可见,请为“播放”和“暂停”媒体事件设置媒体会话操作处理程序,如下所示。

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

显示“上一首”和“下一首”窗口控件的方式类似。为这些操作设置媒体会话操作处理程序后,系统会在画中画窗口中显示它们,然后您将能够处理这些操作。

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

如需查看实际用例,可试用官方媒体会话示例

获取画中画窗口大小

如果您想在视频进入和退出画中画时调整视频质量,则需要知道画中画窗口的大小,并在用户手动调整窗口大小时收到通知。

以下示例展示了如何获取在创建画中画窗口或调整窗口大小时获取该窗口的宽度和高度。

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

我建议不要直接关联到调整大小事件,因为对画中画窗口大小的每个细微更改都会触发一个单独的事件,如果您在每次调整大小时执行开销很大的操作,该事件可能会导致性能问题。换句话说,调整大小操作将会非常快速地反复触发事件。建议使用节流和去抖动等常用技术来解决此问题。

功能支持

Picture-in-Picture Web API 可能不受支持,因此您必须检测此 API 才能提供渐进式增强。即使该功能受支持,用户也可能会将其停用或被权限政策停用。幸运的是,您可以使用新的布尔值 document.pictureInPictureEnabled 来确定这一点。

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

应用于视频的特定按钮元素时,您可能需要如何处理画中画按钮的可见性。

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

MediaStream 视频支持

视频播放的 MediaStream 对象(例如 getUserMedia()getDisplayMedia()canvas.captureStream())在 Chrome 71 中也支持画中画。这意味着您可以显示一个画中画窗口,其中包含用户的摄像头视频流、显示视频流,甚至画布元素。请注意,视频元素无需附加到 DOM 即可进入画中画模式,如下所示。

在“画中画”窗口中显示用户的摄像头

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

在“画中画”窗口中显示显示内容

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

在画中画窗口中显示画布元素

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();

// Later on, video.requestPictureInPicture();

例如,结合使用 canvas.captureStream()Media Session API,您可以在 Chrome 74 中创建音频播放列表窗口。欢迎查看官方音频播放列表示例

画中画窗口中的音频播放列表
图 2. 画中画窗口中的音频播放列表

示例、演示和 Codelab

查看我们的官方画中画示例来试用 Picture-in-Picture Web API。

后续会有演示和 Codelab。

后续步骤

首先,查看实现状态页面,了解当前已在 Chrome 和其他浏览器中实现此 API 的哪些部分。

近期,我们将推出以下功能:

浏览器支持

Chrome、Edge、Opera 和 Safari 支持 Picture-in-Picture Web API。 如需了解详情,请参阅 MDN

资源

非常感谢 Mounir Lamouri 和 Jennifer Apacible 对画中画功能所做的工作以及本文的相关帮助。非常感谢参与标准化工作的所有人。