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

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.
});

如需了解实际操作,请尝试官方 Media Session 示例

获取画中画窗口大小

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

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

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

请查看我们的官方画中画示例,试用画中画 Web API。

后续还会推出演示和 Codelab。

后续步骤

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

近期将发生的变化如下:

浏览器支持

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

资源

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