Auf einem aufgenommenen Tab scrollen und zoomen

François Beaufort
François Beaufort

Auf der Webplattform ist das Teilen von Tabs, Fenstern und Bildschirmen bereits mit der Screen Capture API möglich. Wenn eine Web-App getDisplayMedia() aufruft, wird der Nutzer von Chrome aufgefordert, einen Tab, ein Fenster oder einen Bildschirm als MediaStreamTrack-Video mit der Web-App zu teilen.

In vielen Web-Apps, die getDisplayMedia() verwenden, sehen Nutzer eine Videovorschau der aufgenommenen Oberfläche. So wird das Video in Videokonferenz-Apps oft an Remote-Nutzer gestreamt und gleichzeitig in einem lokalen HTMLVideoElement gerendert. So sieht der lokale Nutzer ständig eine Vorschau dessen, was er freigibt.

In dieser Dokumentation wird die neue Captured Surface Control API in Chrome vorgestellt, mit der Ihre Webanwendung durch einen erfassten Tab scrollen sowie die Zoomstufe eines aufgenommenen Tabs lesen und schreiben kann.

Ein Nutzer scrollt und zoomt auf einem aufgenommenen Tab (Demo).

Vorteile von Captured Surface Control

Alle Videokonferenz-Apps haben denselben Nachteil: Wenn der Nutzer mit einem aufgenommenen Tab oder Fenster interagieren möchte, muss er zu dieser Oberfläche wechseln und die Videokonferenz-App verlassen. Das bringt einige Probleme mit sich:

  • Der Nutzer kann die aufgenommene App und die Videos der Remote-Nutzer nicht gleichzeitig sehen, es sei denn, er verwendet Bild im Bild oder separate Fenster, die auf dem Tab für die Videokonferenz und dem geteilten Tab nebeneinander angezeigt werden. Auf einem kleineren Bildschirm könnte das schwierig sein.
  • Der Nutzer muss dann nicht mehr zwischen der Videokonferenz-App und der aufgenommenen Oberfläche wechseln.
  • Der Nutzer verliert den Zugriff auf die Steuerelemente der Videokonferenz-App, wenn er nicht in der App verfügbar ist. Dazu gehören eine eingebettete Chat-App, Emoji-Reaktionen, Benachrichtigungen über eine Teilnahmeanfrage, Multimedia- und Layoutsteuerelemente sowie andere nützliche Funktionen für Videokonferenzen.
  • Der Vortragende kann die Steuerung nicht an Remote-Teilnehmer delegieren. Dies führt zu einem viel vertrauten Szenario, bei dem Remote-Nutzende die vortragende Person bitten, die Folie zu wechseln, ein wenig nach oben und unten zu scrollen oder den Zoom anzupassen.

Die Captured Surface Control API löst diese Probleme.

Wie verwende ich Captured Surface Control?

Für die erfolgreiche Verwendung von Captured Surface Control sind einige Schritte erforderlich, z. B. müssen Sie einen Browsertab explizit erfassen und die Berechtigung des Nutzers einholen, bevor Sie auf dem aufgenommenen Tab scrollen und zoomen können.

Browsertab aufnehmen

Fordere zuerst den Nutzer auf, eine Oberfläche zum Teilen mit getDisplayMedia() auszuwählen und dabei ein CaptureController-Objekt mit der Aufnahmesitzung zu verknüpfen. Wir werden dieses Objekt bald verwenden, um die erfasste Oberfläche zu kontrollieren.

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

Erstelle als Nächstes eine lokale Vorschau der erfassten Oberfläche in Form eines <video>-Elements:

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

Wenn der Nutzer ein Fenster oder einen Bildschirm freigeben möchte, fällt dies derzeit nicht in den Zuständigkeitsbereich. Wenn er aber einen Tab teilen möchte, können wir fortfahren.

const [track] = stream.getVideoTracks();

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

Berechtigungsaufforderung

Der erste Aufruf von sendWheel() oder setZoomLevel() für ein bestimmtes CaptureController-Objekt erzeugt eine Berechtigungsaufforderung. Wenn der Nutzer die Berechtigung erteilt, sind weitere Aufrufe dieser Methoden für das CaptureController-Objekt zulässig. Wenn der Nutzer die Berechtigung ablehnt, wird das zurückgegebene Promise abgelehnt.

CaptureController-Objekte sind eindeutig einer bestimmten Capture-Sitzung zugeordnet, können keiner anderen Erfassungssitzung zugeordnet werden und überleben die Navigation der Seite, auf der sie definiert sind, nicht. Erfassungssitzungen bleiben jedoch die Navigation auf der erfassten Seite erhalten.

Eine Nutzergeste ist erforderlich, um dem Nutzer eine Berechtigungsaufforderung anzuzeigen. Nur sendWheel()- und setZoomLevel()-Aufrufe erfordern eine Nutzergeste und auch nur dann, wenn die Aufforderung angezeigt werden muss. Wenn der Nutzer in der Web-App auf eine Schaltfläche zum Heran- oder Herauszoomen klickt, ist diese Geste eine vorgegebene Geste. Wenn die App jedoch zuerst eine Scrollsteuerung anbieten möchte, sollten Entwickler bedenken, dass das Scrollen keine Nutzergeste ist. Eine Möglichkeit besteht darin, dem Nutzer zuerst eine Schaltfläche „Scrollen zu starten“ anzubieten, wie im folgenden Beispiel:

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

Scrollen

Mit sendWheel() kann eine Aufnahme-App Radereignisse in der von ihnen gewählten Größe über die von ihnen festgelegten Koordinaten im Darstellungsbereich eines Tabs ausliefern. Das Ereignis ist von der direkten Nutzerinteraktion nicht von der erfassten App zu unterscheiden.

Wenn in der Aufnahme-App ein <video>-Element namens "previewTile" verwendet wird, zeigt der folgende Code, wie Rad-Ereignisse an den erfassten Tab weitergeleitet werden:

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.
    // ...
  }
});

Die Methode sendWheel() verwendet ein Wörterbuch mit zwei Gruppen von Werten:

  • x und y: die Koordinaten, an die das Wheel-Ereignis gesendet werden soll.
  • wheelDeltaX und wheelDeltaY: die Größen der Scrollvorgänge in Pixeln für horizontale bzw. vertikale Scrollvorgänge. Beachten Sie, dass diese Werte im Vergleich zum ursprünglichen wheel-Ereignis invertiert sind.

Eine mögliche Implementierung von translateCoordinates() ist:

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)];
}

Im Code oben gibt es drei verschiedene Größen:

  • Die Größe des <video>-Elements.
  • Die Größe der aufgenommenen Frames (hier als trackSettings.width und trackSettings.height dargestellt).
  • Die Größe des Tabs.

Die Größe des <video>-Elements liegt vollständig innerhalb der Domain der Aufnahme-App und ist dem Browser nicht bekannt. Die Größe des Tabs liegt vollständig innerhalb der Domain des Browsers und ist der Webanwendung nicht bekannt.

Die Web-App verwendet translateCoordinates(), um die Versätze relativ zum <video>-Element in Koordinaten innerhalb des eigenen Koordinatenbereichs des Videotracks umzuwandeln. Der Browser wechselt ebenfalls zwischen der Größe der erfassten Frames und der Größe des Tabs und liefert das Scroll-Ereignis mit einem Offset, der dem von der Web-App erwarteten Offset entspricht.

Das von sendWheel() zurückgegebene Promise kann in den folgenden Fällen abgelehnt werden:

  • Wenn die Aufnahmesitzung noch nicht gestartet oder bereits beendet wurde, einschließlich des Anhaltens asynchron, während die Aktion sendWheel() vom Browser ausgeführt wird.
  • Der Nutzer hat der App keine Berechtigung zur Verwendung von sendWheel() gewährt.
  • Die App zum Erfassen von Bildern versucht, ein Scroll-Ereignis in Koordinaten zu senden, die außerhalb von [trackSettings.width, trackSettings.height] liegen. Beachten Sie, dass sich diese Werte asynchron ändern können. Daher ist es ratsam, den Fehler abzufangen und zu ignorieren. Hinweis: 0, 0 befindet sich normalerweise nicht außerhalb des zulässigen Bereichs. Du kannst es daher bedenkenlos verwenden, um den Nutzer um Erlaubnis zu bitten.

Zoom

Die Interaktion mit der Zoomstufe des aufgenommenen Tabs erfolgt über die folgenden CaptureController-Oberflächen:

  • getSupportedZoomLevels() gibt eine Liste der vom Browser unterstützten Zoomstufen zurück, die als Prozentsätze der „Standard-Zoomstufe“ dargestellt werden, die mit 100 % definiert ist. Diese Liste erhöht sich kontinuierlich und enthält den Wert 100.
  • getZoomLevel() gibt die aktuelle Zoomstufe des Tabs zurück.
  • setZoomLevel() legt die Zoomstufe des Tabs auf einen beliebigen ganzzahligen Wert in getSupportedZoomLevels() fest und gibt ein Promise zurück, wenn der Vorgang erfolgreich ist. Hinweis: Die Zoomstufe wird am Ende der Aufnahme nicht zurückgesetzt.
  • Mit oncapturedzoomlevelchange können Sie Änderungen der Zoomstufe eines erfassten Tabs beobachten, wenn Nutzer die Zoomstufe entweder über die Erfassungs-App oder durch direkte Interaktion mit dem aufgenommenen Tab ändern.

Aufrufe an setZoomLevel() sind durch Berechtigung gesteuert. Aufrufe der anderen schreibgeschützten Zoommethoden sind "kostenlos", ebenso wie das Überwachen von Ereignissen.

Im folgenden Beispiel sehen Sie, wie Sie die Zoomstufe eines aufgenommenen Tabs in einer vorhandenen Aufnahmesitzung erhöhen können:

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.
    // ...
  }
});

Im folgenden Beispiel wird gezeigt, wie Sie auf Änderungen der Zoomstufe eines erfassten Tabs reagieren können:

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

Funktionserkennung

So prüfen Sie, ob das Senden von Rad-Ereignissen unterstützt wird:

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

So prüfen Sie, ob die Zoomsteuerung unterstützt wird:

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

Captured Surface Control aktivieren

Die Captured Surface Control API ist in Chrome auf dem Computer hinter dem Flag „Captured Surface Control“ verfügbar und kann unter chrome://flags/#captured-surface-control aktiviert werden.

Für diese Funktion wird auch ein Ursprungstest gestartet, der mit Chrome 122 auf dem Computer beginnt. Entwickler können die Funktion für Besucher ihrer Websites aktivieren, um Daten von echten Nutzern zu erheben. Weitere Informationen zu Ursprungstests und ihrer Funktionsweise finden Sie unter Erste Schritte mit Ursprungstests.

Sicherheit und Datenschutz

Mit der Berechtigungsrichtlinie für "captured-surface-control" kannst du festlegen, wie deine Aufnahme-App und eingebettete iFrames von Drittanbietern auf Captured Surface Control zugreifen können. Weitere Informationen zu den Vor- und Nachteilen der Sicherheit finden Sie im Abschnitt Datenschutz und Sicherheitsaspekte in der Erläuterung zu „Captured Surface Control“.

Demo

Du kannst Captured Surface Control nutzen, indem du die Demo auf Glitch ausführst. Prüfen Sie unbedingt den Quellcode.

Änderungen gegenüber früheren Versionen von Chrome

Im Folgenden finden Sie einige wichtige Verhaltensunterschiede zu Captured Surface Control, die Sie kennen sollten:

  • In Chrome 124 und niedriger:
    • Die Berechtigung (sofern erteilt) bezieht sich auf die mit der jeweiligen CaptureController verknüpfte Aufnahmesitzung, nicht auf den Ursprung der Aufnahme.
  • In Chrome 122:
    • getZoomLevel() gibt ein Promise mit der aktuellen Zoomstufe des Tabs zurück.
    • sendWheel() gibt ein Promise zurück, das mit der Fehlermeldung "No permission." abgelehnt wurde, wenn der Nutzer der App keine Berechtigung zur Nutzung gewährt hat. Der Fehlertyp ist in Chrome 123 und höher "NotAllowedError".
    • oncapturedzoomlevelchange ist nicht verfügbar. Du kannst für diese Funktion „Polyfill“ mit setInterval() verwenden.

Feedback

Das Chrome-Team und die Webstandards-Community würden gerne mehr über Ihre Erfahrungen mit Captured Surface Control erfahren.

Informationen zum Design

Gibt es etwas an Captured Surface Capture, das nicht wie erwartet funktioniert? Oder fehlen Methoden oder Eigenschaften, die Sie zur Implementierung Ihrer Idee benötigen? Haben Sie eine Frage oder einen Kommentar zum Sicherheitsmodell? Sie können ein Spezifikationsproblem über das GitHub-Repository melden oder Ihre Gedanken zu einem vorhandenen Problem hinzufügen.

Probleme bei der Implementierung?

Haben Sie einen Fehler bei der Implementierung in Chrome gefunden? Oder unterscheidet sich die Implementierung von der Spezifikation? Melden Sie einen Fehler unter https://new.crbug.com. Geben Sie dabei so viele Details wie möglich und eine Anleitung zum Reproduzieren an. Glitch eignet sich hervorragend zum Teilen von reproduzierbaren Fehlern.