Regarder une vidéo en mode Picture-in-picture

François Beaufort
François Beaufort

Le Picture-in-picture (PIP) permet aux utilisateurs de regarder des vidéos dans une fenêtre flottante (toujours au-dessus des autres fenêtres) afin de garder un œil sur ce qu'ils regardent tout en interagissant avec d'autres sites ou applications.

Avec l'API Web Picture-in-picture, vous pouvez démarrer et contrôler le Picture-in-picture pour les éléments vidéo de votre site Web. Essayez-le sur notre exemple Picture-in-picture officiel.

Contexte

En septembre 2016, Safari a ajouté la prise en charge du Picture-in-picture via une API WebKit dans macOS Sierra. Six mois plus tard, Chrome lisait automatiquement les vidéos Picture-in-picture sur mobile avec la sortie d'Android O, basée sur une API Android native. Six mois plus tard, nous avons annoncé notre intention de créer et de standardiser une API Web, compatible avec celle de Safari, qui permettrait aux développeurs Web de créer et de contrôler l'expérience complète autour du Picture-in-picture. Et voilà !

Saisissez le code

Utiliser le mode Picture-in-picture

Commençons par un élément vidéo et un moyen pour l'utilisateur d'interagir avec lui, comme un élément de bouton.

<video id="videoElement" src="https://example.com/file.mp4"></video>
<button id="pipButtonElement"></button>

Ne demandez le Picture-in-picture qu'en réponse à un geste de l'utilisateur, et jamais dans la promesse renvoyée par videoElement.play(). En effet, les promesses ne propagent pas encore les gestes des utilisateurs. Appelez plutôt requestPictureInPicture() dans un gestionnaire de clics sur pipButtonElement, comme indiqué ci-dessous. Il vous incombe de gérer ce qui se passe si un utilisateur clique deux fois.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  await videoElement.requestPictureInPicture();

  pipButtonElement.disabled = false;
});

Lorsque la promesse est résolue, Chrome réduit la vidéo dans une petite fenêtre que l'utilisateur peut déplacer et positionner sur d'autres fenêtres.

Vous avez terminé. Bravo ! Arrêtez de lire et prenez vos vacances bien méritées. Malheureusement, ce n'est pas toujours le cas. La promesse peut être rejetée pour l'une des raisons suivantes:

  • La fonctionnalité Picture-in-picture n'est pas compatible avec ce système.
  • Ce document n'est pas autorisé à utiliser le mode Picture-in-picture en raison d'une règle d'autorisation restrictive.
  • Les métadonnées de la vidéo n'ont pas encore été chargées (videoElement.readyState === 0).
  • Le fichier vidéo ne contient que l'audio.
  • Le nouvel attribut disablePictureInPicture est présent dans l'élément vidéo.
  • L'appel n'a pas été effectué dans un gestionnaire d'événements de geste de l'utilisateur (par exemple, un clic sur un bouton). À partir de Chrome 74, cette option n'est applicable que s'il n'existe pas encore d'élément Picture-in-picture.

La section Compatibilité des fonctionnalités ci-dessous explique comment activer/désactiver un bouton en fonction de ces restrictions.

Ajoutons un bloc try...catch pour capturer ces erreurs potentielles et informer l'utilisateur de ce qui se passe.

pipButtonElement.addEventListener('click', async function () {
  pipButtonElement.disabled = true;

  try {
    await videoElement.requestPictureInPicture();
  } catch (error) {
    // TODO: Show error message to user.
  } finally {
    pipButtonElement.disabled = false;
  }
});

L'élément vidéo se comporte de la même manière, qu'il soit en mode Picture-in-picture ou non: les événements sont déclenchés et les méthodes d'appel fonctionnent. Elle reflète les changements d'état dans la fenêtre Picture-in-picture (par exemple, lecture, pause, recherche, etc.). Il est également possible de modifier l'état par programmation en JavaScript.

Quitter le mode Picture-in-picture

À présent, nous allons activer ou désactiver le mode Picture-in-picture pour notre bouton. Nous devons d'abord vérifier si l'objet en lecture seule document.pictureInPictureElement est notre élément vidéo. Si ce n'est pas le cas, nous vous envoyons une demande pour utiliser le Picture-in-picture, comme indiqué ci-dessus. Sinon, nous vous demandons de quitter la page en appelant document.exitPictureInPicture(), ce qui signifie que la vidéo réapparaîtra dans l'onglet d'origine. Notez que cette méthode renvoie également une promesse.

    ...
    try {
      if (videoElement !== document.pictureInPictureElement) {
        await videoElement.requestPictureInPicture();
      } else {
        await document.exitPictureInPicture();
      }
    }
    ...

Écouter des événements Picture-in-picture

Les systèmes d'exploitation limitent généralement le mode Picture-in-picture à une seule fenêtre. L'implémentation de Chrome suit donc ce schéma. Cela signifie que les utilisateurs ne peuvent lire qu'une seule vidéo Picture-in-picture à la fois. Vous devez vous attendre à ce que les utilisateurs quittent le Picture-in-picture même si vous ne l'avez pas demandé.

Les nouveaux gestionnaires d'événements enterpictureinpicture et leavepictureinpicture nous permettent d'adapter l'expérience aux utilisateurs. Il peut s'agir de parcourir un catalogue de vidéos ou d'afficher un chat en direct.

videoElement.addEventListener('enterpictureinpicture', function (event) {
  // Video entered Picture-in-Picture.
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  // Video left Picture-in-Picture.
  // User may have played a Picture-in-Picture video from a different page.
});

Personnaliser la fenêtre Picture-in-picture

Chrome 74 accepte les boutons de lecture/pause, de piste précédente et de piste suivante dans la fenêtre Picture-in-picture que vous pouvez contrôler à l'aide de l'API Media Session.

Commandes de lecture multimédia dans une fenêtre Picture-in-picture
Image 1. Commandes de lecture multimédia dans une fenêtre Picture-in-picture

Par défaut, un bouton de lecture/pause est toujours affiché dans la fenêtre Picture-in-picture, sauf si la vidéo lit des objets MediaStream (par exemple, getUserMedia(), getDisplayMedia() ou canvas.captureStream()) ou si la durée MediaSource de la vidéo est définie sur +Infinity (par exemple, le flux en direct). Pour vous assurer qu'un bouton de lecture/pause est toujours visible, définissez des gestionnaires d'actions de sessions multimédias pour les événements multimédias "Lecture" et "Pause", comme ci-dessous.

// Show a play/pause button in the Picture-in-Picture window
navigator.mediaSession.setActionHandler('play', function () {
  // User clicked "Play" button.
});
navigator.mediaSession.setActionHandler('pause', function () {
  // User clicked "Pause" button.
});

L'affichage des commandes de la fenêtre "Titre précédent" et "Titre suivant" est similaire. Si vous définissez des gestionnaires d'actions de sessions multimédias pour ces utilisateurs, ceux-ci s'afficheront dans la fenêtre Picture-in-picture, et vous pourrez les gérer.

navigator.mediaSession.setActionHandler('previoustrack', function () {
  // User clicked "Previous Track" button.
});

navigator.mediaSession.setActionHandler('nexttrack', function () {
  // User clicked "Next Track" button.
});

Pour observer son fonctionnement, essayez l'exemple de session multimédia officiel.

Obtenir la taille de la fenêtre Picture-in-picture

Si vous souhaitez ajuster la qualité de la vidéo lors de l'ouverture ou de la fermeture du mode Picture-in-picture, vous devez connaître la taille de la fenêtre Picture-in-picture et être averti si un utilisateur la redimensionne manuellement.

L'exemple ci-dessous montre comment obtenir la largeur et la hauteur de la fenêtre Picture-in-picture lorsqu'elle est créée ou redimensionnée.

let pipWindow;

videoElement.addEventListener('enterpictureinpicture', function (event) {
  pipWindow = event.pictureInPictureWindow;
  console.log(`> Window size is ${pipWindow.width}x${pipWindow.height}`);
  pipWindow.addEventListener('resize', onPipWindowResize);
});

videoElement.addEventListener('leavepictureinpicture', function (event) {
  pipWindow.removeEventListener('resize', onPipWindowResize);
});

function onPipWindowResize(event) {
  console.log(
    `> Window size changed to ${pipWindow.width}x${pipWindow.height}`
  );
  // TODO: Change video quality based on Picture-in-Picture window size.
}

Je vous conseille de ne pas associer directement l'événement de redimensionnement, car chaque petite modification apportée à la taille de la fenêtre Picture-in-picture déclenche un événement distinct qui peut causer des problèmes de performances si vous effectuez une opération coûteuse à chaque redimensionnement. En d'autres termes, l'opération de redimensionnement déclenchera les événements très rapidement. Je vous recommande d'utiliser des techniques courantes telles que la limitation et la rétrogradation pour résoudre ce problème.

Compatibilité des caractéristiques

L'API Picture-in-picture Web n'est peut-être pas compatible. Vous devez donc la détecter pour fournir une amélioration progressive. Même lorsqu'elle est prise en charge, elle peut être désactivée par l'utilisateur ou désactivée par une règle d'autorisation. Heureusement, vous pouvez utiliser la nouvelle valeur booléenne document.pictureInPictureEnabled pour le déterminer.

if (!('pictureInPictureEnabled' in document)) {
  console.log('The Picture-in-Picture Web API is not available.');
} else if (!document.pictureInPictureEnabled) {
  console.log('The Picture-in-Picture Web API is disabled.');
}

Appliqué à un élément de bouton spécifique pour une vidéo, c'est ainsi que vous souhaitez gérer la visibilité du bouton Picture-in-picture.

if ('pictureInPictureEnabled' in document) {
  // Set button ability depending on whether Picture-in-Picture can be used.
  setPipButton();
  videoElement.addEventListener('loadedmetadata', setPipButton);
  videoElement.addEventListener('emptied', setPipButton);
} else {
  // Hide button if Picture-in-Picture is not supported.
  pipButtonElement.hidden = true;
}

function setPipButton() {
  pipButtonElement.disabled =
    videoElement.readyState === 0 ||
    !document.pictureInPictureEnabled ||
    videoElement.disablePictureInPicture;
}

Compatibilité avec les vidéos MediaStream

Les objets MediaStream de lecture vidéo (par exemple, getUserMedia(), getDisplayMedia() ou canvas.captureStream()) sont également compatibles avec le Picture-in-picture dans Chrome 71. Cela signifie que vous pouvez afficher une fenêtre Picture-in-picture contenant le flux vidéo de la webcam ou l'écran de l'utilisateur, voire un élément canevas. Notez qu'il n'est pas nécessaire que l'élément vidéo soit associé au DOM pour entrer dans le mode Picture-in-picture, comme illustré ci-dessous.

Afficher la webcam de l'utilisateur dans la fenêtre Picture-in-picture

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getUserMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Afficher l'écran dans la fenêtre Picture-in-picture

const video = document.createElement('video');
video.muted = true;
video.srcObject = await navigator.mediaDevices.getDisplayMedia({video: true});
video.play();

// Later on, video.requestPictureInPicture();

Afficher l'élément de canevas dans la fenêtre Picture-in-picture

const canvas = document.createElement('canvas');
// Draw something to canvas.
canvas.getContext('2d').fillRect(0, 0, canvas.width, canvas.height);

const video = document.createElement('video');
video.muted = true;
video.srcObject = canvas.captureStream();
video.play();

// Later on, video.requestPictureInPicture();

En combinant canvas.captureStream() et l'API Media Session, vous pouvez par exemple créer une fenêtre de playlist audio dans Chrome 74. Consultez l'exemple de playlist audio officiel.

Playlist audio dans une fenêtre Picture-in-picture
Figure 2 : Playlist audio dans une fenêtre Picture-in-picture

Exemples, démonstrations et ateliers de programmation

Consultez notre exemple Picture-in-picture officiel pour essayer l'API Web Picture-in-picture.

Des démonstrations et des ateliers de programmation suivront.

Et ensuite ?

Tout d'abord, consultez la page État de l'implémentation pour savoir quelles parties de l'API sont actuellement implémentées dans Chrome et dans d'autres navigateurs.

Voici ce à quoi vous pouvez vous attendre prochainement:

Prise en charge des navigateurs

L'API Picture-in-picture Web est compatible avec Chrome, Edge, Opera et Safari. Pour en savoir plus, consultez la page MDN.

Ressources

Un grand merci à Mounir Lamouri et à Jennifer Apacible pour leur travail sur le mode Picture-in-picture, et pour leur aide pour cet article. Un grand merci à toutes les personnes qui ont participé à l'effort de standardisation.