捲動及縮放已擷取的分頁

François Beaufort
François Beaufort

使用者可透過 Screen Capture API 分享分頁、視窗及螢幕畫面。當網頁應用程式呼叫 getDisplayMedia() 時,Chrome 會提示使用者以 MediaStreamTrack 影片的形式與網頁應用程式分享分頁、視窗或螢幕畫面。

許多使用 getDisplayMedia() 的網頁應用程式都會向使用者顯示已擷取介面的影片預覽畫面。舉例來說,視訊會議應用程式通常會向遠端使用者串流播放這部影片,同時將影片內容轉譯至本機的 HTMLVideoElement,讓本機使用者能夠持續預覽他們分享的內容。

本文件將介紹 Chrome 新推出的 Captured Surface Control API,讓網頁應用程式捲動已擷取的分頁,以及讀取和寫入已擷取分頁的縮放等級。

使用者捲動及縮放已擷取的分頁 (示範)。

為什麼要使用 Captured Surface Control?

所有視訊會議應用程式都受到同樣的缺點:如果使用者希望與擷取的分頁或視窗互動,就必須切換至該介面,將其退出視訊會議應用程式。這有一些挑戰:

  • 使用者無法同時看到擷取的應用程式和遠端使用者的影片,除非他們使用「子母畫面」,或是獨立用於視訊會議分頁和共用分頁的並排視窗。在較小的螢幕上,這可能並不容易。
  • 使用者需要在視訊會議應用程式和擷取的介面之間來回切換,感到不堪負荷。
  • 使用者離開視訊會議後,他們就無法再存取視訊會議應用程式所公開的控制項;例如內嵌的即時通訊應用程式、表情符號回應、有關使用者要求加入通話的通知、多媒體和版面配置控制項,以及其他實用的視訊會議功能。
  • 簡報者無法將控制權交給遠端參與者。這會產生非常熟悉的情形,遠端使用者會請簡報者變更投影片、上下捲動或調整縮放等級。

Captured Surface Control API 可解決這些問題。

如何使用擷取的表面控制?

成功使用 Capture Surface Control 前,必須完成幾個步驟,例如明確擷取瀏覽器分頁並向使用者取得權限,才能捲動及縮放已擷取的分頁。

擷取瀏覽器分頁

首先,請提示使用者使用 getDisplayMedia() 選擇要共用的介面,然後在過程中將 CaptureController 物件與擷取工作階段建立關聯。我們會盡快使用該物件控制擷取的表面。

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

接著,以 <video> 元素的形式,為已擷取的表面產生本機預覽畫面:

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 物件上的這些方法。如果使用者拒絕授予權限,系統就會拒絕傳回的承諾。

請注意,CaptureController 物件僅與特定capture-session相關聯,而且不能與其他擷取工作階段建立關聯,也無法在定義其所在的頁面後繼續留存。不過,擷取工作階段在瀏覽擷取的網頁後「確實」仍然存在。

必須設置使用者手勢,才能向使用者顯示權限提示。只有 sendWheel()setZoomLevel() 呼叫需要使用者手勢,且只在需要顯示提示時才會出現。如果使用者在網頁應用程式中按一下放大或縮小按鈕,系統會採用該手勢。但如果應用程式希望先提供捲動控制項,則開發人員應該注意,捲動會構成使用者手勢。其中一種可能做法是先為使用者提供「開始捲動」按鈕,如下列範例所示:

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> 元素的偏移值轉譯為視訊軌自身座標空間內的座標。瀏覽器也會在擷取的影格大小和分頁大小之間進行轉換,並以與網頁應用程式預期方向對應的偏移值傳遞捲動事件。

在下列情況中,sendWheel() 傳回的承諾可能會遭到拒絕:

  • 如果擷取工作階段尚未啟動或已停止,包括在瀏覽器處理 sendWheel() 動作時以非同步方式停止。
  • 如果使用者未授予應用程式使用 sendWheel() 的權限。
  • 如果擷取的應用程式嘗試在 [trackSettings.width, trackSettings.height] 以外的座標提供捲動事件。請注意,這些值可能會以非同步方式變更,因此最好擷取並忽略錯誤。(請注意,0, 0 通常不會超出範圍,因此您可以使用其提示使用者授予權限)。

縮放

透過下列 CaptureController 介面,與已擷取分頁的縮放等級互動:

  • getSupportedZoomLevels() 會傳回瀏覽器支援的縮放等級清單,以「預設縮放等級」(定義為 100%) 的百分比表示。這份清單為單調遞增,其中包含 100 值。
  • getZoomLevel() 會傳回分頁目前的縮放等級。
  • setZoomLevel() 會將分頁的縮放等級設為 getSupportedZoomLevels() 中的任何整數值,並在成功時傳回承諾。請注意,擷取工作階段結束時,系統不會重設縮放等級。
  • 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 Control API 可在 Chrome 電腦版中使用 Captured Surface Control 旗標後方,使用者也可前往 chrome://flags/#captured-surface-control 啟用。

這項功能也正從電腦版的 Chrome 122 開始提供來源試用。這項功能可讓開發人員啟用這項功能,讓網站訪客收集實際使用者的資料。如要進一步瞭解來源試用和運作方式,請參閱「開始使用來源試用」一文。

安全性和隱私權

"captured-surface-control" 權限政策可讓您管理擷取應用程式和內嵌第三方 iframe 如何存取 Captured Surface Control。如要瞭解安全性的取捨,請參閱「擷取表面控制項」說明中的「隱私權和安全性考量」一節。

示範

您可以在 Glitch 上執行示範,藉此使用 Captured Surface Control 遊戲。請務必檢查原始碼

與舊版 Chrome 不同的變更

以下是您應留意的 Captured Surface Control 的一些主要行為差異:

  • Chrome 124 以下版本:
    • 權限 (如有授予) 範圍僅限於與 CaptureController 相關聯的擷取工作階段,而非擷取來源。
  • 在 Chrome 122 中:
    • getZoomLevel() 會傳回分頁目前縮放等級的承諾值。
    • 如果使用者未授予應用程式使用權限,sendWheel() 會傳回拒絕結果,並顯示錯誤訊息 "No permission."。在 Chrome 123 以上版本中,錯誤類型為 "NotAllowedError"
    • 無法使用「oncapturedzoomlevelchange」。您可以使用 setInterval() 將此功能多填。

意見回饋

Chrome 團隊和網路標準社群想瞭解你的 Captured Surface Control 使用體驗,

請與我們分享設計

你遇到的 Captured Surface Capture 功能不如預期運作的問題?或是你還需要實現創意的方法或屬性嗎?對安全性模型有任何疑問或意見嗎?在 GitHub 存放區上提交規格問題,或將你的想法新增至現有問題。

無法導入嗎?

您發現 Chrome 實作錯誤嗎?還是採用與規格不同?前往 https://new.crbug.com 回報錯誤。請盡可能附上所有細節,以及重現問題的操作說明。Glitch 很適合用來分享可重現的錯誤。