滚动和缩放拍摄的标签页

François Beaufort
François Beaufort

使用 Screen Capture API 可以在网络平台上共享标签页、窗口和屏幕。当某个 Web 应用调用 getDisplayMedia() 时,Chrome 会提示用户以 MediaStreamTrack 视频的形式与 Web 应用分享标签页、窗口或屏幕。

许多使用 getDisplayMedia() 的 Web 应用会向用户显示截取的界面的视频预览。例如,视频会议应用通常会将该视频流式传输给远程用户,同时将其呈现给本地 HTMLVideoElement,以便本地用户不断预览他们分享的内容。

本文档介绍了 Chrome 中新增的 Captured Surface Control API。借助该 API,您的 Web 应用可以滚动捕获的标签页,以及读取和写入已捕获的标签页的缩放级别。

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
用户滚动和缩放截取的标签页(演示)。

为何使用 Captured Surface Control?

所有视频会议应用都有相同的缺点:如果用户希望与截取的标签页或窗口互动,则必须切换到该界面,使其离开视频会议应用。这就带来了一些挑战:

  • 用户无法同时查看拍摄的应用和远程用户的视频,除非他们使用画中画功能或为视频会议标签页和“共享”标签页设置单独的并排窗口。在较小的屏幕上,这可能并非易事。
  • 用户需要在视频会议应用和捕获的界面之间切换。
  • 用户无法再使用视频会议应用提供的控件,即使他们没有该应用,例如,嵌入式聊天应用、表情符号回应、用户请求加入通话的通知、多媒体和布局控件以及其他实用的视频会议功能。
  • 演示者无法将控制权委托给远程参与者。这就导致了一种非常熟悉的情况,即远程用户要求演示者更改幻灯片、上下滚动或者调整缩放级别。

Captured Surface Control API 可解决这些问题。

如何使用 Captured Surface Control?

成功使用“捕获的 Surface 控件”需要执行几个步骤,例如明确捕获浏览器标签页,以及从用户处获取权限,然后才能滚动和缩放捕获的标签页。

截取浏览器标签页

首先提示用户使用 getDisplayMedia() 选择要分享的 surface,并在此过程中将 CaptureController 对象与拍摄会话相关联。我们会尽快使用该对象控制捕获的表面。

const controller = new CaptureController();
const stream = await navigator.mediaDevices.getDisplayMedia({ controller });

接下来,以 <video> 元素的形式生成所拍摄 Surface 的本地预览:

const previewTile = document.querySelector('video');
previewTile.srcObject = stream;

如果用户选择共享窗口或屏幕,这目前不在支持范围内,但如果他们选择共享标签页,我们可以继续。

const [track] = stream.getVideoTracks();

if (track.getSettings().displaySurface !== 'browser') {
  // Bail out early if the user didn't pick a tab.
  return;
}

权限提示

首次对给定 CaptureController 对象调用 sendWheel()setZoomLevel() 时会生成权限提示。如果用户授予权限,系统将允许进一步调用相应 CaptureController 对象上的这些方法。如果用户拒绝授予权限,则返回的 promise 会被拒绝。

请注意,CaptureController 对象与特定的capture-session唯一关联,不能与其他捕获会话相关联,并且在定义它们的网页导航后也不会继续存在。不过,捕获会话在被捕获的网页导航之后仍会继续存在。

您需要使用用户手势操作,才能向用户显示权限提示。只有 sendWheel()setZoomLevel() 调用需要用户手势,并且仅在需要显示提示时才需要。如果用户点击 Web 应用中的放大或缩小按钮,就给定该用户手势;但是,如果应用想要首先提供滚动控件,则开发者应记住,滚动构成用户手势。一种可能是首先为用户提供“开始滚动”按钮,如以下示例所示:

const startScrollingButton = document.querySelector('button');

startScrollingButton.addEventListener('click', async () => {
  try {
    const noOpWheelAction = {};

    await controller.sendWheel(noOpWheelAction);
    // The user approved the permission prompt.
    // You can now scroll and zoom the captured tab as shown later in the article.
  } catch (error) {
    return; // Permission denied. Bail.
  }
});

滚动

使用 sendWheel(),捕获应用可以通过其在标签页视口内选择的坐标传递其所选幅度的滚轮事件。对于捕获的应用来说,该事件与直接用户互动难以区分。

假设捕获应用使用名为 "previewTile"<video> 元素,以下代码展示了如何将发送轮事件中继到捕获的标签页:

const previewTile = document.querySelector('video');

previewTile.addEventListener('wheel', async (event) => {
  // Translate the offsets into coordinates which sendWheel() can understand.
  // The implementation of this translation is explained further below.
  const [x, y] = translateCoordinates(event.offsetX, event.offsetY);
  const [wheelDeltaX, wheelDeltaY] = [-event.deltaX, -event.deltaY];

  try {
    // Relay the user's action to the captured tab.
    await controller.sendWheel({ x, y, wheelDeltaX, wheelDeltaY });
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

sendWheel() 方法接受包含两组值的字典:

  • xy:车轮事件要传送到的坐标。
  • wheelDeltaXwheelDeltaY:分别针对水平和垂直滚动的滚动幅度(以像素为单位)。请注意,与原始 wheel 事件相比,这些值是相反的。

translateCoordinates() 的可能实现是:

function translateCoordinates(offsetX, offsetY) {
  const previewDimensions = previewTile.getBoundingClientRect();
  const trackSettings = previewTile.srcObject.getVideoTracks()[0].getSettings();

  const x = trackSettings.width * offsetX / previewDimensions.width;
  const y = trackSettings.height * offsetY / previewDimensions.height;

  return [Math.floor(x), Math.floor(y)];
}

请注意,在之前的代码中,有三种不同的尺寸:

  • <video> 元素的大小。
  • 捕获的帧的大小(此处表示为 trackSettings.widthtrackSettings.height)。
  • 标签页的大小。

<video> 元素的大小完全在捕获应用的网域中,浏览器是未知的。标签页的大小完全在浏览器的网域中,网络应用是未知的。

Web 应用使用 translateCoordinates() 将相对于 <video> 元素的偏移量转换为视频轨道自身坐标空间内的坐标。浏览器同样会在捕获的帧大小和标签页大小之间转换,并以与 Web 应用预期的相对应的偏移量传递滚动事件。

在以下情况下,sendWheel() 返回的 promise 可能会被拒绝:

  • 拍摄会话尚未开始或已停止(包括在浏览器处理 sendWheel() 操作时异步停止)。
  • 如果用户未授予应用使用 sendWheel() 的权限。
  • 如果捕获应用尝试在 [trackSettings.width, trackSettings.height] 之外的坐标中传递滚动事件。请注意,这些值可能会异步更改,因此最好捕获错误并忽略它。(请注意,0, 0 通常不会超出边界,因此您可以放心使用它们来提示用户授予权限。)

缩放

您可以通过以下 CaptureController surface 与所截取标签页的缩放级别进行互动:

  • getSupportedZoomLevels() 会返回浏览器支持的缩放级别列表,以占“默认缩放级别”(定义为 100%)的百分比表示。此列表是单调递增的,其值为 100。
  • getZoomLevel() 会返回标签页的当前缩放级别。
  • setZoomLevel() 用于将标签页的缩放级别设置为 getSupportedZoomLevels() 中存在的任何整数值,并在操作成功时返回 promise。请注意,缩放级别不会在拍摄会话结束时重置。
  • oncapturedzoomlevelchange 可让您监听捕获的标签页的缩放级别变化,因为用户可能会通过捕获应用或与所捕获的标签页的直接交互来更改缩放级别。

setZoomLevel() 的调用受权限控制;对其他只读缩放方法的调用是“免费”的,就像监听事件一样。

<ph type="x-smartling-placeholder">

下例展示了如何在现有拍摄会话中增加所拍摄标签页的缩放级别:

const zoomIncreaseButton = document.getElementById('zoomInButton');

zoomIncreaseButton.addEventListener('click', async (event) => {
  const levels = CaptureController.getSupportedZoomLevels();
  const index = levels.indexOf(controller.getZoomLevel());
  const newZoomLevel = levels[Math.min(index + 1, levels.length - 1)];

  try {
    await controller.setZoomLevel(newZoomLevel);
  } catch (error) {
    // Inspect the error.
    // ...
  }
});

下例显示了如何响应所拍摄标签页的缩放级别变化:

controller.addEventListener('capturedzoomlevelchange', (event) => {
  const zoomLevel = controller.getZoomLevel();
  document.querySelector('#zoomLevelLabel').textContent = `${zoomLevel}%`;
});

功能检测

如需检查是否支持发送轮盘事件,请使用以下命令:

if (!!window.CaptureController?.prototype.sendWheel) {
  // CaptureController sendWheel() is supported.
}

如需检查是否支持控制缩放,请使用:

if (!!window.CaptureController?.prototype.setZoomLevel) {
  // CaptureController setZoomLevel() is supported.
}

启用“Captured Surface”控件

在桌面版 Chrome 中,Captured Surface Control API 位于 Captured Surface Control 标志的后面,并可通过 chrome://flags/#captured-surface-control 启用。

此外,这项功能也已进入源试用阶段,桌面版 Chrome 122 将开始进行初始试用;开发者可借助此功能让自己网站的访问者从真实用户那里收集数据。如需详细了解源试用及其运作方式,请参阅源试用使用入门

安全和隐私设置

借助 "captured-surface-control" 权限政策,您可以管理捕获应用和嵌入式第三方 iframe 如何访问捕获的 Surface 控件。如需了解安全方面的权衡取舍,请参阅“捕获的 Surface 控件”说明文档中的隐私权和安全注意事项部分。

演示

您可以通过运行 Glitch 上的演示来体验 Captured Surface Control。请务必查看源代码

与旧版 Chrome 相比的变化

以下是您应该了解的关于 Captured Surface Control 的一些关键行为差异:

  • 在 Chrome 124 及更低版本中: <ph type="x-smartling-placeholder">
      </ph>
    • 该权限(如果已授予)的范围是与该 CaptureController 关联的捕获会话,而非捕获源。
  • 在 Chrome 122 中: <ph type="x-smartling-placeholder">
      </ph>
    • getZoomLevel() 会返回一个包含标签页当前缩放级别的 promise。
    • 如果用户未向应用授予使用权限,sendWheel() 会返回一个遭拒的 promise,并显示错误消息 "No permission."。在 Chrome 123 及更高版本中,错误类型为 "NotAllowedError"
    • oncapturedzoomlevelchange不可用。您可以使用 setInterval() 对此功能执行 polyfill 操作。

反馈

Chrome 团队和网络标准社区希望了解您使用 Captured Surface Control 的体验。

向我们介绍设计

关于“Captured Surface Capture”的某些方面是否无法按预期运行?或者,您是否缺少实现自己的想法所需的方法或属性?对安全模型有疑问或意见?在 GitHub 代码库中提交规范问题,或者添加您对现有问题的看法。

实施时遇到问题?

您在 Chrome 的实现过程中是否发现了错误?或者,实现是否与规范不同?访问 https://new.crbug.com 提交 bug。请务必提供尽可能多的细节,以及重现说明。Glitch 非常适用于分享可重现的错误。