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 dans Chrome, qui permet à votre application Web de faire défiler un onglet capturé et 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 en même temps l'application capturée et les vidéos des utilisateurs distants, sauf s'ils utilisent le mode Picture-in-picture ou des fenêtres côte à côte distinctes pour l'onglet "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 trop familier où les utilisateurs à distance demandent au présentateur de changer la diapositive, de faire défiler un peu vers le haut et 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, par exemple 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 refusé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. Cependant, 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 diffuser des événements de roue de sa magnitude choisie plutôt que des coordonnées de son choix dans la fenêtre d'affichage 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 roue 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() utilise un dictionnaire contenant deux ensembles de valeurs:

  • x et y : coordonnées où l'événement de la roue doit être envoyé.
  • wheelDeltaX et wheelDeltaY: magnitudes des défilements horizontaux et verticaux, en pixels. Notez que ces valeurs sont inversées par rapport à l'événement de 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 lorsqu'elle aboutit. 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 changements de niveau de zoom d'un onglet capturé :

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

Détection de caractéristiques

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 pris en charge, utilisez :

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

Activer le contrôle de 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 refusée avec le message d'erreur "No permission." si l'utilisateur n'a pas accordé l'autorisation d'utilisation à l'application. 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.

Dites-nous en plus sur la conception

Y a-t-il un élément Captured Surface Capture qui ne fonctionne pas comme prévu ? Ou s'il manque des méthodes ou des propriétés dont vous avez besoin pour mettre en œuvre 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.

Vous rencontrez un problème lors de l'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 la page https://new.crbug.com. Veillez à fournir autant de détails que possible, ainsi que des instructions pour le reproduire. Glitch fonctionne particulièrement bien pour partager des bugs reproductibles.