滚动和缩放拍摄的标签页

François Beaufort
François Beaufort

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

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

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

用户滚动和缩放拍摄的标签页(演示)。

为何使用“捕获的表面控制”?

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

  • 除非远程用户为视频会议标签页和“共享”标签页使用画中画或单独的并排窗口,否则用户无法同时查看拍摄的应用和远程用户的视频。在小屏幕设备上,这可能比较困难。
  • 由于需要在视频会议应用和拍摄的界面之间来回切换,用户会面临各种各样的管理任务。
  • 用户离开视频会议时,无法使用其公开的控件,例如嵌入式聊天应用、表情符号回应、关于用户请求加入通话的通知、多媒体和布局控件,以及其他实用的视频会议功能。
  • 演示者无法将控制权委托给远程参与者。这就引出了一种非常熟悉的场景,远程用户会要求演示者更改幻灯片、稍微上下滚动,或者调整缩放级别。

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

如何使用“捕获的表面控制”?

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

截取浏览器标签页

首先,使用 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 对象与特定的拍摄会话唯一关联,无法与其他拍摄会话关联,在浏览定义它们的网页后也不会继续保留。不过,捕获会话在所捕获网页的导航结束后仍然有效。

需要通过用户手势来向用户显示权限提示。只有 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> 元素,以下代码展示了如何将“send wheel”事件中继到捕获的标签页:

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 further explained 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> 元素的大小完全在捕获应用的网域中,并且浏览器不知道。标签页的大小完全在浏览器的网域中,对 Web 应用而言是未知的。

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() 的调用受权限控制;对其他只读缩放方法的调用是“自由”的,与监听事件一样。

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

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

启用捕获的 Surface 控制

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

此外,此功能也开始进入桌面版 Chrome 122 的源试用,以便开发者为其网站的访问者启用该功能,以收集真实用户的数据。如需详细了解源试用及其工作原理,请参阅源试用开始

安全和隐私设置

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

演示

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

与旧版 Chrome 相比的变化

以下是关于捕获的 Surface 控件的一些关键行为差异,您应注意:

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

反馈

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

请告诉我们设计情况

关于“捕获的 Surface Capture”是否存在无法按预期运行的问题?或者,是否缺少一些方法或属性来实施您的想法?对安全模型有疑问或意见?在 GitHub 代码库中提交规范问题,或将您的想法添加到现有问题中。

实施方面有问题?

您是否发现了 Chrome 实现方面的错误?或者,实现方式是否不同于规范?请通过 https://new.crbug.com 提交 bug。请务必提供尽可能多的详细信息以及重现说明。Glitch 非常适用于分享可重现的错误。