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

François Beaufort
François Beaufort

La fonctionnalité 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 mode Picture-in-Picture pour les éléments vidéo de votre site Web. Essayez-la sur notre exemple officiel Picture-in-picture.

Contexte

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

Se plonger dans le code

Activer le mode Picture-in-picture

Commençons simplement 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 mode Picture-in-picture qu'en réponse à un geste de l'utilisateur, et jamais selon 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 par-dessus d'autres fenêtres.

Vous avez terminé. Bravo ! Vous pouvez arrêter de lire et partir en vacances bien méritées. Malheureusement, ce n'est pas toujours le cas. La promesse peut être refusée pour l'une des raisons suivantes:

  • Le système n'est pas compatible avec le mode Picture-in-picture.
  • Le 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 est audio uniquement.
  • Le nouvel attribut disablePictureInPicture est présent sur l'élément vidéo.
  • L'appel n'a pas été effectué dans un gestionnaire d'événements de gestes de l'utilisateur (par exemple, un clic sur un bouton). À partir de Chrome 74, cela n'est applicable que si aucun élément n'est déjà présent dans le Picture-in-picture.

La section Compatibilité avec les 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. Il reflète les changements d'état dans la fenêtre Picture-in-picture (par exemple, la lecture, la mise en pause, la recherche, etc.). Il est également possible de modifier l'état de manière programmatique en JavaScript.

Quitter le mode Picture-in-picture

Maintenant, nous allons faire en sorte que notre bouton passe en mode Picture-in-picture. 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 envoyons une requête pour activer le mode Picture-in-Picture, comme indiqué ci-dessus. Sinon, nous demandons à quitter la vidéo 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 les é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 modèle. Cela signifie que les utilisateurs ne peuvent lire qu'une seule vidéo Picture-in-picture à la fois. Attendez-vous à ce que les utilisateurs quittent le mode 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 de diffusion 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 est compatible avec les boutons de lecture/pause, de piste précédente et de piste suivante dans la fenêtre Picture-in-picture. Vous pouvez contrôler ce paramètre à 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 s'affiche toujours 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 de la MediaSource est définie sur +Infinity (par exemple, flux en direct). Pour vous assurer qu'un bouton de lecture/pause est toujours visible, définissez des gestionnaires d'action de session multimédia pour les événements multimédias "Play" (Lecture) et "Pause" (Pause), comme indiqué 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 fenêtre "Piste précédente" et "Piste suivante" est semblable. Définir des gestionnaires d'actions de session multimédia pour ceux-ci les affichera dans la fenêtre Picture-in-picture, et vous pourrez gérer ces actions.

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

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

Pour voir comment cela fonctionne, essayez l'exemple de session multimédia officiel.

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

Si vous souhaitez ajuster la qualité vidéo lorsque la vidéo passe en mode Picture-in-picture et en sort, vous devez connaître la taille de la fenêtre Picture-in-picture et être averti si un utilisateur redimensionne manuellement la fenêtre.

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 suggère de ne pas vous connecter 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 entraîner 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 de manière répétée et très rapidement. Je vous recommande d'utiliser des techniques courantes telles que le lissage et le débouncing pour résoudre ce problème.

Compatibilité des caractéristiques

Il est possible que l'API Web Picture-in-Picture ne soit pas compatible. Vous devez donc le détecter pour fournir une amélioration progressive. Même lorsqu'elle est compatible, 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, voici comment vous pouvez gérer la visibilité de votre 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é vidéo avec MediaStream

Les objets MediaStream de lecture vidéo (par exemple, getUserMedia(), getDisplayMedia() et canvas.captureStream()) sont également compatibles avec le mode 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 de l'utilisateur, le flux vidéo de l'écran ou même un élément de canevas. Notez que l'élément vidéo n'a pas besoin d'être associé au DOM pour passer en 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'affichage 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 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() avec 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 officiel Picture-in-picture pour essayer l'API Web Picture-in-picture.

Des démonstrations et des ateliers de programmation suivront.

Étapes suivantes

Tout d'abord, consultez la page d'état de l'implémentation pour connaître les parties de l'API actuellement implémentées dans Chrome et dans d'autres navigateurs.

Voici ce à quoi vous pouvez vous attendre dans un avenir proche:

Prise en charge des navigateurs

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

Ressources

Merci beaucoup à Mounir Lamouri et à Jennifer Apacible pour leur travail sur le mode Picture-in-Picture et leur aide pour cet article. Et un grand merci à tous ceux qui ont participé à l'effort de normalisation.