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 était déjà possible sur la plate-forme Web avec l'API Capture d'écran. Lorsqu'une application Web appelle getDisplayMedia(), Chrome invite l'utilisateur à partager un onglet, une fenêtre ou un écran avec l'application Web sous forme de vidéo MediaStreamTrack.

De nombreuses applications Web qui utilisent getDisplayMedia() présentent à l'utilisateur un aperçu vidéo de la surface capturée. Par exemple, les applications de visioconférence lisent souvent cette vidéo aux utilisateurs distants tout en la affichant dans un HTMLVideoElement local, de sorte que l'utilisateur local voit en permanence un aperçu du contenu qu'il partage.

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

Un utilisateur fait défiler un onglet capturé et zoome (démonstration).

Pourquoi utiliser Captured Surface Control ?

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 basculer vers cette surface, ce qui l'éloigne de l'application de visioconférence. Cela présente certains problèmes:

  • L'utilisateur ne peut pas voir en même temps l'application capturée et les vidéos des utilisateurs distants, sauf s'il utilise le mode Picture-in-picture ou des fenêtres côte à côte distinctes pour l'onglet "Visioconférence" et l'onglet "Partagé". Cela peut être difficile sur un écran de petite taille.
  • L'utilisateur est contrarié par le besoin de passer de l'application de visioconférence à la surface capturée.
  • L'utilisateur perd l'accès aux commandes affichées par l'application de visioconférence lorsqu'il n'y est pas. Par exemple, une application de chat intégrée, les réactions emoji, les notifications d'utilisateurs demandant à participer à l'appel, les commandes multimédias et de mise en page, ainsi que d'autres fonctionnalités de visioconférence utiles.
  • Le présentateur ne peut pas déléguer le contrôle à des participants distants. Cela conduit au scénario trop familier dans lequel les utilisateurs distants demandent au présentateur de changer de 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 Captured Surface Control ?

L'utilisation réussie du contrôle de la surface capturée nécessite quelques étapes. Par exemple, vous pouvez 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 du navigateur

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

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

Ensuite, générez 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'est pas possible pour le moment. Toutefois, 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

Le premier appel de sendWheel() ou setZoomLevel() sur un objet CaptureController donné génère une invite d'autorisation. Si l'utilisateur accorde cette autorisation, les 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 capture-session 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 existent après la navigation sur la page capturée.

Un geste 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 être affichée. Si l'utilisateur clique sur un bouton de zoom avant ou arrière dans l'application Web, ce geste est évident. Toutefois, si l'application souhaite d'abord proposer une commande de défilement, les développeurs doivent garder à l'esprit que le défilement ne constitue pas un geste de l'utilisateur. Une possibilité consiste d'abord à proposer à l'utilisateur un bouton "commencer à faire défiler", 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

Avec sendWheel(), une application de capture peut diffuser des événements de roue de la magnitude choisie sur les coordonnées de son choix dans la fenêtre d'affichage d'un onglet. Impossible de distinguer l'événement de l'application capturée 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 relayer des événements en forme de roue à 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 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.
    // ...
  }
});

La méthode sendWheel() utilise un dictionnaire contenant deux ensembles de valeurs:

  • x et y: coordonnées où l'événement en forme de roue doit être diffusé.
  • wheelDeltaX et wheelDeltaY: amplitudes 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 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 entrent en jeu 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> correspond entièrement au domaine de l'application de capture et n'est pas connue du navigateur. La taille de l'onglet dépend entièrement du domaine du navigateur et l'application Web ne la reconnaît pas.

L'application Web utilise translateCoordinates() pour traduire les décalages relatifs à l'élément <video> en coordonnées dans l'espace de coordonnées de la piste vidéo. Le navigateur effectue également une conversion entre la taille des frames capturés et celle de l'onglet, et diffuse l'événement de défilement avec un décalage correspondant à la taille attendue de l'application Web.

La promesse renvoyée par sendWheel() peut être refusé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 s'il s'arrête de manière asynchrone pendant que le navigateur gère l'action sendWheel().
  • Si l'utilisateur n'a pas autorisé l'application à utiliser sendWheel().
  • Si l'application de capture tente de diffuser un événement de défilement dans des coordonnées en dehors de [trackSettings.width, trackSettings.height]. Notez que ces valeurs peuvent changer de manière asynchrone. Il est donc recommandé d'identifier l'erreur et de l'ignorer. (Notez que 0, 0 ne serait normalement pas hors limites. Vous pouvez donc les utiliser sans risque pour demander une autorisation à l'utilisateur.)

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 sous forme de pourcentages du "niveau de zoom par défaut", défini sur 100%. Cette liste augmente de manière monotone et contient la valeur 100.
  • getZoomLevel() affiche 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 lorsque l'opération réussit. Notez que le niveau de zoom n'est pas réinitialisé à la fin de la session de capture.
  • oncapturedzoomlevelchange vous permet d'écouter les changements de niveau de zoom d'un onglet capturé, car les utilisateurs peuvent modifier le niveau de zoom via l'application d'enregistrement ou via une interaction directe avec l'onglet capturé.

Les appels à setZoomLevel() sont contrôlés par une autorisation. Les appels vers les autres méthodes de zoom en lecture seule sont "sans frais", tout comme l'écoute des événements.

L'exemple suivant vous 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 fonctionnalités

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

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

Pour vérifier s'il est possible de contrôler le zoom, utilisez:

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

Activer Captured Surface Control

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

Cette fonctionnalité entre également dans une phase d'évaluation commençant avec 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 phases d'évaluation et leur fonctionnement, consultez Premiers pas avec les phases d'évaluation.

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 tiers intégrés à Captured Surface Control. Pour comprendre les compromis en matière de sécurité, consultez la section Considérations liées à la confidentialité et à la sécurité de la vidéo d'explication sur le contrôle de la surface capturée.

Démonstration

Pour jouer avec Captured Surface Control, exécutez la démonstration sur Glitch. N'oubliez pas de consulter le code source.

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

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

  • Dans Chrome 124 et versions antérieures :
    • Si l'autorisation est accordée, elle est limitée à la session de capture associée à ce 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 autorisé l'application à l'utiliser. Le type d'erreur est "NotAllowedError" dans Chrome 123 et versions ultérieures.
    • oncapturedzoomlevelchange n'est pas disponible. Vous pouvez émuler cette caractéristique à l'aide de setInterval().

Commentaires

L'équipe Chrome et la communauté des normes Web souhaitent en savoir plus sur votre expérience avec Captured Surface Control.

Parlez-nous de la conception

Y a-t-il un problème avec la capture de surface capturée qui ne fonctionne pas comme prévu ? Ou manque-t-il 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 spécifique dans le dépôt GitHub ou faites part de vos commentaires à un problème existant.

Un problème d'implémentation ?

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