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

François Beaufort
François Beaufort

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

借助 Picture-in-Picture Web API,您可以启动和控制网站上视频元素的画中画功能。您可以使用我们的官方画中画示例试用此功能。

背景

2016 年 9 月,Safari 通过 macOS Sierra 中的 WebKit API 添加了画中画支持。六个月后,随着 Android O 的发布,Chrome 开始使用原生 Android API 在移动设备上自动播放画中画视频。六个月后,我们宣布了我们的意图,即构建并标准化一个与 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 会将视频缩小到一个小窗口中,用户可以移动该窗口并将其置于其他窗口之上。

大功告成。太棒了!您可以停止阅读,去享受您应得的假期。遗憾的是,情况并非总是如此。系统可能会出于以下任一原因拒绝执行承诺:

  • 系统不支持画中画。
  • 由于权限政策限制,文档不允许使用画中画功能。
  • 尚未加载视频元数据 (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(例如直播动态)。为确保播放/暂停按钮始终可见,请为“播放”和“暂停”媒体事件设置 Somesee 媒体会话操作处理脚本,如下所示。

// 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.
}

建议您不要直接钩接到大小调整事件,因为对画中画窗口大小进行的每项小更改都会触发单独的事件,如果您在每次调整大小时执行耗时操作,可能会导致性能问题。换句话说,调整大小操作将非常快速地反复触发事件。建议您使用节流和调试等常用技术来解决此问题。

功能支持

系统可能不支持画中画 Web 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 视频支持

在 Chrome 71 中,播放 MediaStream 对象(例如 getUserMedia()getDisplayMedia()canvas.captureStream())的视频也支持画中画功能。这意味着您可以显示一个画中画窗口,其中包含用户的摄像头视频串流、显示视频流,甚至画布元素。请注意,视频元素无需附加到 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 支持画中画 Web API。 如需了解详情,请参阅 MDN

资源

非常感谢 Mounir Lamouri 和 Jennifer Apacible 对“画中画”工具所做的工作以及本文提供的帮助。衷心感谢参与标准化工作的所有人。