Faire défiler un onglet capturé et effectuer un zoom

François Beaufort
François Beaufort

Le partage d'onglets, de fenêtres et d'écrans est déjà possible sur la plate-forme Web avec l'API Screen Capture. Lorsqu'une application Web appelle getDisplayMedia(), Chrome invite l'utilisateur à partager un onglet, une fenêtre ou un écran avec l'application Web en tant que vidéo MediaStreamTrack.

De nombreuses applications Web qui utilisent getDisplayMedia() affichent un aperçu vidéo de la surface capturée. Par exemple, les applications de visioconférence diffusent souvent cette vidéo en streaming auprès des utilisateurs distants tout en l'affichant sur un HTMLVideoElement local, afin que l'utilisateur local voit constamment un aperçu de ce qu'il partage.

Cette documentation présente la nouvelle API Captured Surface Control (API de contrôle de la surface capturée) dans Chrome, qui permet à votre application Web de faire défiler un onglet capturé, ainsi que de lire et d'écrire le niveau de zoom d'un onglet capturé.

Un utilisateur fait défiler et fait un zoom sur un onglet capturé (démo).

Pourquoi utiliser le contrôle de la surface capturée ?

Toutes les applications de visioconférence présentent le même inconvénient: si l'utilisateur souhaite interagir avec un onglet ou une fenêtre capturés, il doit passer à cette surface, ce qui l'éloigne de l'application de visioconférence. Cela présente certains défis:

  • L'utilisateur ne peut pas voir l'application capturée et les vidéos des utilisateurs distants en même temps, sauf s'il utilise la fonctionnalité Picture-in-picture (Mode Picture-in-picture) ou des fenêtres côte à côte distinctes pour l'onglet de la visioconférence et l'onglet partagé. Sur un écran de petite taille, cela peut être difficile.
  • L'utilisateur doit passer de l'application de visioconférence à la surface capturée.
  • L'utilisateur perd l'accès aux commandes exposées par l'application de visioconférence lorsqu'il n'y accède pas (par exemple, une application de chat intégrée, des réactions emoji, des notifications concernant des utilisateurs demandant à participer à l'appel, des commandes multimédias et de mise en page, et d'autres fonctionnalités de visioconférence utiles).
  • Le présentateur ne peut pas déléguer le contrôle aux participants à distance. Cela conduit au scénario bien connu où les utilisateurs à distance demandent au présentateur de changer de diapositive, de faire défiler la page vers le haut ou vers le bas, ou d'ajuster le niveau de zoom.

L'API Captured Surface Control résout ces problèmes.

Comment utiliser le contrôle de la surface capturée ?

Pour utiliser le contrôle de la surface capturée, vous devez suivre quelques étapes, comme capturer explicitement un onglet de navigateur et obtenir l'autorisation de l'utilisateur avant de pouvoir faire défiler et zoomer l'onglet capturé.

Capturer un onglet de navigateur

Commencez par demander à l'utilisateur de choisir une surface à partager à l'aide de getDisplayMedia(), puis associez un objet CaptureController à la session de capture. Nous utiliserons bientôt cet objet pour contrôler la surface capturée.

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

Ensuite, créez un aperçu local de la surface capturée sous la forme d'un élément <video>:

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

Si l'utilisateur choisit de partager une fenêtre ou un écran, cela n'entre pas dans le champ d'application pour le moment. En revanche, s'il choisit de partager un onglet, nous pouvons continuer.

const [track] = stream.getVideoTracks();

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

Invite d'autorisation

La première invocation de sendWheel() ou setZoomLevel() sur un objet CaptureController donné génère une invite d'autorisation. Si l'utilisateur accorde l'autorisation, d'autres appels de ces méthodes sur cet objet CaptureController sont autorisés. Si l'utilisateur refuse l'autorisation, la promesse renvoyée est rejetée.

Notez que les objets CaptureController sont associés de manière unique à une session de capture spécifique, ne peuvent pas être associés à une autre session de capture et ne survivent pas à la navigation sur la page où ils sont définis. Toutefois, les sessions de capture survivent à la navigation sur la page capturée.

Un geste de l'utilisateur est requis pour afficher une invite d'autorisation. Seuls les appels sendWheel() et setZoomLevel() nécessitent un geste de l'utilisateur, et uniquement si l'invite doit s'afficher. Si l'utilisateur clique sur un bouton de zoom avant ou arrière dans l'application Web, ce geste utilisateur est donné. Toutefois, si l'application souhaite d'abord proposer un contrôle de défilement, les développeurs doivent garder à l'esprit que le défilement ne constitue pas un geste utilisateur. Vous pouvez d'abord proposer à l'utilisateur un bouton "Démarrer le défilement", comme dans l'exemple suivant:

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

Faire défiler

À l'aide de sendWheel(), une application de capture peut envoyer des événements de roue de la magnitude de son choix sur les coordonnées de son choix dans le viewport d'un onglet. Pour l'application capturée, l'événement est impossible à distinguer de l'interaction directe de l'utilisateur.

En supposant que l'application de capture utilise un élément <video> appelé "previewTile", le code suivant montre comment transmettre des événements de défilement à l'onglet capturé:

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

La méthode sendWheel() prend un dictionnaire avec deux ensembles de valeurs:

  • x et y: coordonnées où l'événement de la roue doit être envoyé.
  • wheelDeltaX et wheelDeltaY: valeurs des défilements, en pixels, pour les défilements horizontaux et verticaux, respectivement. Notez que ces valeurs sont inversées par rapport à l'événement de la roue d'origine.

Voici une implémentation possible de 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)];
}

Notez que trois tailles différentes sont utilisées dans le code précédent:

  • Taille de l'élément <video>.
  • Taille des images capturées (représentées ici par trackSettings.width et trackSettings.height).
  • Taille de l'onglet.

La taille de l'élément <video> est entièrement dans le domaine de l'application de capture et inconnue du navigateur. La taille de l'onglet est entièrement dans le domaine du navigateur et inconnue de l'application Web.

L'application Web utilise translateCoordinates() pour traduire les décalages par rapport à l'élément <video> en coordonnées dans l'espace de coordonnées de la piste vidéo. Le navigateur effectuera également la conversion entre la taille des cadres capturés et la taille de l'onglet, et transmettra l'événement de défilement avec un décalage correspondant aux attentes de l'application Web.

La promesse renvoyée par sendWheel() peut être rejetée dans les cas suivants:

  • Si la session de capture n'a pas encore commencé ou s'est déjà arrêtée, y compris si elle s'arrête de manière asynchrone pendant que l'action sendWheel() est gérée par le navigateur.
  • Si l'utilisateur n'a pas autorisé l'application à utiliser sendWheel().
  • Si l'application de capture tente de générer un événement de défilement dans des coordonnées situées en dehors de [trackSettings.width, trackSettings.height]. Notez que ces valeurs peuvent changer de manière asynchrone. Il est donc conseillé de détecter l'erreur et de l'ignorer. (Notez que 0, 0 ne devrait normalement pas être en dehors de la plage, vous pouvez donc l'utiliser pour demander à l'utilisateur l'autorisation.)

Zoom

L'interaction avec le niveau de zoom de l'onglet capturé s'effectue via les surfaces CaptureController suivantes:

  • getSupportedZoomLevels() renvoie une liste des niveaux de zoom compatibles avec le navigateur, représentés en pourcentage du "niveau de zoom par défaut", défini sur 100%. Cette liste augmente de façon linéaire et contient la valeur 100.
  • getZoomLevel() renvoie le niveau de zoom actuel de l'onglet.
  • setZoomLevel() définit le niveau de zoom de l'onglet sur n'importe quelle valeur entière présente dans getSupportedZoomLevels() et renvoie une promesse en cas de réussite. Notez que le niveau de zoom n'est pas réinitialisé à la fin de la session de capture.
  • oncapturedzoomlevelchange vous permet d'écouter les modifications du niveau de zoom d'un onglet capturé, car les utilisateurs peuvent modifier le niveau de zoom via l'application de capture ou par interaction directe avec l'onglet capturé.

Les appels à setZoomLevel() sont soumis à autorisation. Les appels aux autres méthodes de zoom en lecture seule sont "libres", tout comme l'écoute des événements.

L'exemple suivant montre comment augmenter le niveau de zoom d'un onglet capturé dans une session de capture existante:

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

L'exemple suivant vous montre comment réagir aux modifications de niveau de zoom d'un onglet capturé:

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

Détection de fonctionnalités

Pour vérifier si l'envoi d'événements de roue est compatible, utilisez:

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

Pour vérifier si le contrôle du zoom est compatible, utilisez:

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

Activer le contrôle de la surface capturée

L'API Captured Surface Control est disponible dans Chrome sur ordinateur derrière l'indicateur Captured Surface Control et peut être activée à l'emplacement chrome://flags/#captured-surface-control.

Cette fonctionnalité est également en phase d'évaluation à partir de Chrome 122 sur ordinateur, ce qui permet aux développeurs d'activer la fonctionnalité pour les visiteurs de leurs sites afin de collecter des données auprès d'utilisateurs réels. Pour en savoir plus sur les essais d'origine et leur fonctionnement, consultez Premiers pas avec les essais d'origine.

Sécurité et confidentialité

La règle d'autorisation "captured-surface-control" vous permet de gérer l'accès de votre application de capture et des iFrames tierces intégrés au contrôle de la surface capturée. Pour comprendre les compromis de sécurité, consultez la section Considérations sur la confidentialité et la sécurité de la vidéo expliquant le contrôle des surfaces capturées.

Démo

Vous pouvez tester la commande de surface capturée en exécutant la démo sur Glitch. N'oubliez pas de consulter le code source.

Modifications par rapport aux versions précédentes de Chrome

Voici quelques différences de comportement importantes que vous devez connaître concernant le contrôle de la surface capturée:

  • Dans Chrome 124 et versions antérieures :
    • L'autorisation, si elle est accordée, est limitée à la session de capture associée à cet CaptureController, et non à l'origine de la capture.
  • Dans Chrome 122 :
    • getZoomLevel() renvoie une promesse avec le niveau de zoom actuel de l'onglet.
    • sendWheel() renvoie une promesse rejetée avec le message d'erreur "No permission." si l'utilisateur n'a pas accordé à l'application l'autorisation d'utiliser l'appareil. Le type d'erreur est "NotAllowedError" dans Chrome 123 et versions ultérieures.
    • oncapturedzoomlevelchange n'est pas disponible. Vous pouvez utiliser setInterval() pour effectuer un polyfill de cette fonctionnalité.

Commentaires

L'équipe Chrome et la communauté des normes Web souhaitent connaître votre expérience avec le contrôle de la surface capturée.

Parlez-nous de la conception

La capture de surface capturée ne fonctionne-t-elle pas comme prévu ? Ou manque-t-il des méthodes ou des propriétés dont vous avez besoin pour implémenter votre idée ? Vous avez une question ou un commentaire sur le modèle de sécurité ? Signalez un problème de spécification dans le dépôt GitHub ou ajoutez vos commentaires à un problème existant.

Problème d'implémentation ?

Avez-vous trouvé un bug dans l'implémentation de Chrome ? Ou l'implémentation est-elle différente de la spécification ? Signalez un bug sur https://new.crbug.com. Veillez à inclure autant de détails que possible, ainsi que des instructions pour reproduire le problème. Glitch est l'outil idéal pour partager des bugs reproductibles.