使用 Captured Surface Control API

本指南介紹如何使用 Captured Surface Control API 提供的功能來控制由螢幕捕獲 API捕獲的顯示錶面(瀏覽器標籤頁、視窗或螢幕)。

Background

螢幕捕獲 API 最常用於在會議應用中與其他參與者共享裝置上的另一個開啟的標籤頁或視窗,例如演示新功能或展示報告。

這有一個顯著的問題是,當您想與捕獲的顯示錶面交互時,例如滾動或縮放它,您必須切換到捕獲的顯示錶面才能進行操作。這會造成幾個問題,並使應用程式比必要時更令人沮喪。螢幕共享使用者會發現自己不得不在會議應用程式和捕獲的顯示錶面之間來回切換,以調整媒體顯示、允許遲到的使用者加入、閱讀聊天訊息等。

Captured Surface Control API 透過允許應用程式開發人員實現一組有限的功能來解決這些問題,會議參與者可以直接在應用程式內使用這些功能來控制捕獲的顯示錶面,而不會危及安全性。

目前這些功能包括

  1. 縮放捕獲的顯示錶面。
  2. 使用滑鼠滾輪/觸控板手勢(及其他等效操作)滾動捕獲的顯示錶面。

所有這些功能都透過 CaptureController 物件進行訪問。要控制捕獲的顯示錶面,必須在 MediaDevices.getDisplayMedia() 呼叫的 options 物件中傳入一個 capture controller。

js
controller = new CaptureController();

const displayMediaOptions = {
  controller,
};

videoElem.srcObject =
  await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);

然後可以使用該 controller 來,例如,放大捕獲的顯示錶面。

js
controller.increaseZoomLevel();

在本文中,我們將逐步介紹一個基本螢幕共享應用程式的程式碼,該應用程式展示瞭如何實現這些功能。

關於許可權的說明

網站可以使用 Permissions-Policy captured-surface-control 指令,或等效的 <iframe> allow 屬性值來控制對 Captured Surface Control API 的訪問。

html
<iframe allow="captured-surface-control" src="/some-other-document.html">
  ...
</iframe>

具體來說,forwardWheel()increaseZoomLevel()decreaseZoomLevel()resetZoomLevel() 方法受此指令控制。

captured-surface-control 的預設允許列表是 self,它允許同一來源內的任何內容使用 Captured Surface Control。

如果網站策略允許許可權,使用者可以授予(或拒絕)訪問受控 API 的許可權。這可以是顯式許可權,透過響應提示授予,也可以是透過與呼叫其中一個方法(瞬時啟用)的控制元件互動而隱式授予,前提是使用者未明確拒絕許可權。

另請參閱 螢幕捕獲 API > 安全注意事項

應用 HTML

我們示例應用程式的標記如下:

html
<h1>Captured Surface Control API demo</h1>

<p>
  <button id="start">Start Capture</button>
  <button id="stop">Stop Capture</button>
</p>
<p id="zoom-controls">
  <button id="dec">Zoom -</button>
  <output>100%</output>
  <button id="inc">Zoom +</button>
  <button id="reset">Reset zoom</button>
</p>

<video autoplay></video>

這包含兩組 <button> 元素——一組用於開始和停止螢幕捕獲,另一組用於控制捕獲的顯示錶面的縮放。後者還包括一個 <output> 元素,用於顯示當前的縮放級別。

最後,我們包含一個 <video> 元素來顯示捕獲的顯示錶面。

應用 CSS

應用程式的 CSS 非常簡約;值得注意的是,我們為 <video> 設定了 100%max-width,使其能夠限制在 <body> 內部。當捕獲的顯示錶面嵌入到其中時(其大小是捕獲的固有大小),<video> 可能會急劇增長,如果不加以限制,可能會導致溢位問題。

css
body {
  max-width: 640px;
  margin: 0 auto;
}

video {
  max-width: 100%;
}

初始設定

在我們指令碼的第一部分,我們定義了設定應用程式所需的變數。

js
// Grab references to the <video> element and zoom controls
const videoElem = document.querySelector("video");
const zoomControls = document.getElementById("zoom-controls");

// Grab references to the start and stop capture buttons
const startBtn = document.getElementById("start");
const stopBtn = document.getElementById("stop");

// Grab references to the zoom out, in, and reset buttons,
// and the zoom level output
const decBtn = document.getElementById("dec");
const outputElem = document.querySelector("output");
const incBtn = document.getElementById("inc");
const resetBtn = document.getElementById("reset");

// Define variables to store the controller and the zoom levels
// in, when we later create them
let controller = undefined;
let zoomLevels = undefined;

然後,我們透過將表面控制元件欄的 display CSS 屬性設定為 none 來初始隱藏它,並透過將停止按鈕的 disabled 屬性設定為 true 來停用它。這些控制元件在捕獲開始之前是不相關的,因此我們不希望在開始時顯示它們而混淆使用者。

js
zoomControls.style.display = "none";
stopBtn.disabled = true;

控制螢幕捕獲

接下來,我們向開始和停止按鈕新增 click 事件監聽器(使用 EventTarget.addEventListener()),以便在按下它們時開始和停止螢幕捕獲。

js
startBtn.addEventListener("click", startCapture);
stopBtn.addEventListener("click", stopCapture);

startCapture() 函式用於開始螢幕捕獲,如下所示。我們首先建立一個新的 CaptureController,並將其與 displaySurface 約束一起,作為 MediaDisplayOptions 物件的一部分傳遞,該約束導致應用程式推薦共享瀏覽器標籤頁。

現在是捕獲媒體的時候了;我們使用 MediaDevices.getDisplayMedia() 呼叫來完成此操作,我們將選項傳遞給它,並將返回的 promise 設定為 <video> 元素的 srcObject 屬性的值。當它解析時,我們透過呼叫 CaptureController.resetZoomLevel() 並將 <output> 元素的內容設定為 100% 來繼續執行函式。這並非嚴格必需,但在捕獲標籤頁時發現它已經被縮小或放大可能會有點令人困惑。在捕獲時將縮放級別設定為 100% 感覺更合乎邏輯。這些程式碼行處理了應用程式在未按“停止捕獲”的情況下重新整理,然後再次啟動捕獲的情況。

下一步,我們呼叫 CaptureController.getSupportedZoomLevels() 來檢索捕獲的顯示錶面支援的縮放級別,並將返回的陣列儲存在 zoomLevels 變數中。

接下來,我們使用 controller 的 zoomlevelchange 事件來檢測縮放級別何時發生變化,將當前的 zoomLevel 寫入 <output> 元素,並呼叫使用者定義的 updateZoomButtonState() 函式。此函式將查詢 zoomLevels 陣列,以檢查使用者在每次縮放更改後是否還可以進一步縮放或縮小。稍後我們將解釋 updateZoomButtonState()

我們接下來使用 display: block 來取消隱藏縮放控制元件,啟用停止按鈕,並停用開始按鈕,以便在捕獲開始後控制元件的狀態變得有意義。

為了完成函式,我們呼叫 CaptureController.setFocusBehavior() 來阻止焦點在捕獲開始時轉移到捕獲的顯示錶面,並呼叫我們使用者定義的 startForwarding() 函式來啟用使用滾輪/觸控板手勢滾動捕獲的顯示錶面。稍後我們將解釋此函式。

js
async function startCapture() {
  try {
    // Create a new CaptureController instance
    controller = new CaptureController();

    // Options for getDisplayMedia()
    const displayMediaOptions = {
      controller,
      video: {
        displaySurface: "browser",
      },
    };

    // Capture a tab and display it inside the video element
    videoElem.srcObject =
      await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);

    // Reset the zoom level when capture starts
    controller.resetZoomLevel();
    outputElem.textContent = `100%`;

    // Get zoom levels for the current captured display surface
    zoomLevels = controller.getSupportedZoomLevels();

    // Report zoom level when it changes
    controller.addEventListener("zoomlevelchange", () => {
      outputElem.textContent = `${controller.zoomLevel}%`;
      updateZoomButtonState();
    });

    zoomControls.style.display = "block";
    stopBtn.disabled = false;
    startBtn.disabled = true;

    // Stop the focus from jumping to the captured tab, if you are self-sharing
    controller.setFocusBehavior("focus-capturing-application");

    // Start forwarding wheel events
    startForwarding();
  } catch (e) {
    console.error(e);
  }
}

現在來定義 stopCapture() 函式,該函式用於停止螢幕捕獲。我們在函式開始時再次呼叫 CaptureController.resetZoomLevel() 並將 <output> 元素的內容設定為 100%,以便重置縮放級別。這處理了透過按“停止捕獲”來停止捕獲然後再次開始捕獲的情況。

然後,我們遍歷與 MediaStreamstop() 相關的所有 MediaStreamTrack 物件。然後,我們呼叫 resetApp() 函式,該函式將 <video> 元素的 srcObject 設定回 null,隱藏縮放控制元件,停用停止按鈕,並啟用開始按鈕。

js
function stopCapture() {
  let tracks = videoElem.srcObject.getTracks();
  tracks.forEach((track) => track.stop());
  resetApp();
}

function resetApp() {
  videoElem.srcObject = null;
  zoomControls.style.display = "none";
  stopBtn.disabled = true;
  startBtn.disabled = false;
}

實現縮放控制元件

在我們指令碼的下一部分,我們將縮放按鈕連線到相應的 click 處理程式函式,以便我們可以放大和縮小捕獲的顯示錶面。它們被點選時執行的函式如下:

js
decBtn.addEventListener("click", decreaseZoom);
incBtn.addEventListener("click", increaseZoom);
resetBtn.addEventListener("click", resetZoom);

async function decreaseZoom() {
  try {
    await controller.decreaseZoomLevel();
  } catch (e) {
    console.log(e);
  }
}

async function increaseZoom() {
  try {
    await controller.increaseZoomLevel();
  } catch (e) {
    console.log(e);
  }
}

async function resetZoom() {
  await controller.resetZoomLevel();
}

注意: 通常,最好將 decreaseZoomLevel()increaseZoomLevel() 呼叫放在 try...catch 塊中,因為縮放級別可能被應用程式以外的實體非同步更改,這可能會導致丟擲錯誤。例如,使用者可能直接與捕獲的表面互動來放大或縮小。

當縮放發生變化時,controller 的 zoomlevelchange 事件會觸發,這會導致 startCapture() 函式中前面看到的程式碼執行,將更新的縮放級別寫入 <output> 元素,並執行 updateZoomButtonState() 函式以阻止使用者縮放過遠。

js
controller.addEventListener("zoomlevelchange", () => {
  outputElem.textContent = `${controller.zoomLevel}%`;
  updateZoomButtonState();
});

將滾輪事件轉發到捕獲的顯示錶面

之前,在 startCapture() 函式的底部,我們運行了 startForwarding() 函式,該函式允許從捕獲應用程式滾動捕獲的顯示錶面。這會執行 CaptureController.forwardWheel() 方法,我們將 <video> 元素的引用傳遞給它。當返回的 promise 解析時,瀏覽器開始將 <video> 上觸發的所有 wheel 事件轉發到捕獲的標籤頁或視窗,以便進行滾動。

js
async function startForwarding() {
  try {
    await controller.forwardWheel(videoElem);
  } catch (e) {
    console.log(e);
  }
}

阻止使用者縮放過遠

最後,我們定義 updateZoomButtonState() 函式,該函式在前面看到的 zoomlevelchange 事件處理程式函式中執行。這個函式解決的問題是,如果你嘗試縮小到低於最低支援縮放級別,或者放大到高於最高支援縮放級別,decreaseZoomLevel()/increaseZoomLevel() 將會丟擲 InvalidStateError DOMException

updateZoomButtonState() 函式透過首先確保“縮小”和“放大”按鈕都已啟用來避免此問題。然後進行兩個檢查:

  • 如果當前縮放級別(由 CaptureController.zoomLevel 屬性返回)等於最低支援縮放級別(儲存在 zoomLevels 陣列的第一個值中),則停用“縮小”按鈕,以便使用者無法進一步縮小。
  • 如果當前縮放級別等於最高支援縮放級別(儲存在 zoomLevels 陣列的最後一個值中),則停用“放大”按鈕,以便使用者無法進一步放大。
js
function updateZoomButtonState() {
  decBtn.disabled = false;
  incBtn.disabled = false;
  if (controller.zoomLevel === zoomLevels[0]) {
    decBtn.disabled = true;
  } else if (controller.zoomLevel === zoomLevels[zoomLevels.length - 1]) {
    incBtn.disabled = true;
  }
}

完成的演示

完成的演示渲染如下: