滚动和缩放拍摄的标签页

François Beaufort
François Beaufort

借助 Screen Capture API,您已经可以在 Web 平台上共享标签页、窗口和屏幕。当 Web 应用调用 getDisplayMedia() 时,Chrome 会提示用户将标签页、窗口或屏幕作为 MediaStreamTrack 视频与 Web 应用分享。

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

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

用户滚动和缩放已截取的标签页(演示)。

为何使用 Captured Surface Control?

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

  • 用户无法同时查看拍摄的应用和远程用户的视频,除非他们使用画中画功能或为视频会议标签页和“共享”标签页设置单独的并排窗口。在较小的屏幕上,这可能会很困难。
  • 用户需要在视频会议应用和截取的界面之间跳转,这给用户带来了负担。
  • 用户离开视频会议应用时,无法再使用视频会议应用提供的控件;例如,嵌入式聊天应用、表情符号回应、有关用户请求加入通话的通知、多媒体和布局控件,以及其他实用的视频会议功能。
  • 演示者无法将控制权委托给远程参与者。这会导致一个我们太熟悉的场景:远程用户要求演示者更改幻灯片、上下滚动一点或调整缩放级别。

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

如何使用 Captured Surface Control?

若要成功使用 Captured Surface Control,需要完成几个步骤,例如明确捕获浏览器标签页并获得用户授权,然后才能滚动和缩放所捕获的标签页。

截取浏览器标签页

首先,提示用户使用 getDisplayMedia() 选择要共享的 Surface,并在此过程中将 CaptureController 对象与捕获会话相关联。我们很快就会使用该对象来控制所捕获的 Surface。

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 对象与特定的捕获会话具有唯一关联,无法与其他捕获会话相关联,并且在其定义的页面上进行导航后会失效。不过,捕获会话在被捕获的网页导航之后仍会继续存在。

必须使用用户手势向用户显示权限提示。只有 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:水平滚动和垂直滚动的滚动幅度(以像素为单位)。请注意,与原始滚轮事件相比,这些值是相反的。

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> 元素的大小完全在捕获应用的网域内,浏览器无法得知。标签页的大小完全在浏览器的网域中,网络应用是未知的。

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

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

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

缩放

与所捕获标签页的缩放级别进行交互是通过以下 CaptureController Surface 完成的:

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

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

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

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 及更低版本中:
    • 该权限(如果已授予)的范围仅限于与该 CaptureController 关联的拍摄会话,而非拍摄来源。
  • 在 Chrome 122 中:
    • getZoomLevel() 会返回一个包含标签页当前缩放级别的 Promise。
    • 如果用户未向应用授予使用权限,sendWheel() 会返回一个被拒绝的 promise,并显示错误消息 "No permission."。在 Chrome 123 及更高版本中,错误类型为 "NotAllowedError"
    • oncapturedzoomlevelchange 不可用。您可以使用 setInterval() 对此功能执行 polyfill 操作。

反馈

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

请说明设计

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

实现方面存在问题?

您是否发现了 Chrome 实现中的 bug?或者实现与规范不同?请访问 https://new.crbug.com 提交 bug。请务必提供尽可能详细的信息,以及重现问题的说明。故障非常适合分享可重现的 bug。