Lecture de vidéos sur le Web mobile

François Beaufort
François Beaufort

Comment créer la meilleure expérience multimédia mobile sur le Web ? Rien de plus simple ! Tout dépend de l'engagement des utilisateurs et de l'importance que vous accordez aux contenus multimédias sur une page Web. Je pense que nous sommes tous d'accord pour dire que si la vidéo constitue LA raison de la visite d'un utilisateur, l'expérience utilisateur doit être immersive et réengager.

lecture vidéo sur le Web mobile

Dans cet article, je vais vous montrer comment améliorer progressivement votre expérience multimédia et la rendre plus immersive grâce à une multitude d'API Web. C'est pourquoi nous allons créer une expérience de lecteur mobile simple avec des commandes personnalisées, un plein écran et une lecture en arrière-plan. Vous pouvez maintenant essayer l'exemple et trouver le code dans notre dépôt GitHub.

Commandes personnalisées

Mise en page HTML
Figure 1 : Mise en page HTML

Comme vous pouvez le voir, la mise en page HTML que nous allons utiliser pour notre lecteur multimédia est assez simple : un élément racine <div> contient un élément multimédia <video> et un élément enfant <div> dédié aux commandes vidéo.

Les commandes vidéo que nous verrons plus tard incluent : un bouton de lecture/pause, un bouton en plein écran, des boutons de recherche avant et arrière, ainsi que des éléments pour l'heure actuelle, la durée et le suivi du temps.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls"></div>
</div>

Lire les métadonnées d'une vidéo

Commençons par attendre que les métadonnées vidéo soient chargées pour définir la durée de la vidéo, l'heure actuelle et initialiser la barre de progression. Notez que la fonction secondsToTimeCode() est une fonction utilitaire personnalisée que j'ai écrite. Elle convertit un nombre de secondes en chaîne au format "hh:mm:ss", ce qui est plus adapté dans notre cas.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong>
      <div id="videoCurrentTime"></div>
      <div id="videoDuration"></div>
      <div id="videoProgressBar"></div>
    </strong>
  </div>
</div>
video.addEventListener('loadedmetadata', function () {
  videoDuration.textContent = secondsToTimeCode(video.duration);
  videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
  videoProgressBar.style.transform = `scaleX(${
    video.currentTime / video.duration
  })`;
});
Métadonnées de vidéo uniquement
Figure 2 : Lecteur multimédia affichant les métadonnées de la vidéo

Lire/Mettre en pause la vidéo

Maintenant que les métadonnées de la vidéo sont chargées, ajoutons notre premier bouton permettant à l'utilisateur de lire et de mettre en pause la vidéo avec video.play() et video.pause() en fonction de son état de lecture.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <strong><button id="playPauseButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
playPauseButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (video.paused) {
    video.play();
  } else {
    video.pause();
  }
});

Plutôt que d'ajuster nos commandes vidéo dans l'écouteur d'événements click, nous utilisons les événements vidéo play et pause. Baser nos commandes sur des événements permet de gagner en flexibilité (comme nous le verrons plus tard avec l'API Media Session) et de synchroniser nos commandes si le navigateur intervient dans la lecture. Lorsque la vidéo commence à être lue, nous définissons l'état du bouton sur "Pause" et masquons les commandes de la vidéo. Lorsque la vidéo est mise en pause, nous changeons simplement l'état du bouton en "Lire" et affichons les commandes vidéo.

video.addEventListener('play', function () {
  playPauseButton.classList.add('playing');
});

video.addEventListener('pause', function () {
  playPauseButton.classList.remove('playing');
});

Lorsque l'heure indiquée par l'attribut currentTime de la vidéo a changé via l'événement vidéo timeupdate, nous mettons également à jour nos commandes personnalisées si elles sont visibles.

video.addEventListener('timeupdate', function () {
  if (videoControls.classList.contains('visible')) {
    videoCurrentTime.textContent = secondsToTimeCode(video.currentTime);
    videoProgressBar.style.transform = `scaleX(${
      video.currentTime / video.duration
    })`;
  }
});

Lorsque la vidéo se termine, nous changeons simplement l'état du bouton en "Lire", rétablissons la valeur currentTime de la vidéo sur 0 et affichons les commandes de la vidéo pour le moment. Notez que nous pouvons également choisir de charger automatiquement une autre vidéo si l'utilisateur a activé une fonctionnalité de "lecture automatique".

video.addEventListener('ended', function () {
  playPauseButton.classList.remove('playing');
  video.currentTime = 0;
});

Avance rapide et retour en arrière

Poursuivez et ajoutez les boutons "Rechercher en arrière" et "Rechercher en avant" afin que l'utilisateur puisse facilement ignorer certains contenus.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <strong
      ><button id="seekForwardButton"></button>
      <button id="seekBackwardButton"></button
    ></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
var skipTime = 10; // Time to skip in seconds

seekForwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
});

seekBackwardButton.addEventListener('click', function (event) {
  event.stopPropagation();
  video.currentTime = Math.max(video.currentTime - skipTime, 0);
});

Comme précédemment, plutôt que d'ajuster le style de la vidéo dans les écouteurs d'événements click de ces boutons, nous utiliserons les événements vidéo seeking et seeked déclenchés pour ajuster la luminosité de la vidéo. Ma classe CSS seeking personnalisée est aussi simple que filter: brightness(0);.

video.addEventListener('seeking', function () {
  video.classList.add('seeking');
});

video.addEventListener('seeked', function () {
  video.classList.remove('seeking');
});

Voici ce que nous avons créé jusqu'à présent. Dans la section suivante, nous allons implémenter le bouton en plein écran.

Plein écran

Nous allons ici exploiter plusieurs API Web pour créer une expérience en plein écran parfaite et fluide. Pour voir un exemple concret, consultez l'exemple.

Bien entendu, vous n'êtes pas obligé d'utiliser toutes ces fonctionnalités. Il vous suffit de choisir celles qui vous conviennent et de les combiner pour créer votre flux personnalisé.

Empêcher le plein écran automatique

Sur iOS, les éléments video passent automatiquement en mode plein écran lorsque la lecture multimédia commence. Comme nous essayons de personnaliser et de contrôler autant que possible notre expérience multimédia dans les navigateurs mobiles, je vous recommande de définir l'attribut playsinline de l'élément video pour le forcer à lire en ligne sur iPhone et à ne pas passer en mode plein écran au début de la lecture. Notez que cela n'a aucun effet secondaire sur les autres navigateurs.

<div id="videoContainer"></div>
  <video id="video" src="file.mp4"></video><strong>playsinline</strong></video>
  <div id="videoControls">...</div>
</div>

Activer/Désactiver le plein écran en cliquant sur un bouton

Maintenant que nous empêchons le plein écran automatique, nous devons gérer nous-mêmes le mode plein écran de la vidéo avec l'API Fullscreen. Lorsque l'utilisateur clique sur le bouton "plein écran", quittons le mode plein écran avec document.exitFullscreen() si le mode plein écran est actuellement utilisé par le document. Sinon, demandez le plein écran sur le conteneur vidéo à l'aide de la méthode requestFullscreen() si disponible ou utilisez webkitEnterFullscreen() pour l'élément vidéo uniquement sur iOS.

<div id="videoContainer">
  <video id="video" src="file.mp4"></video>
  <div id="videoControls">
    <button id="playPauseButton"></button>
    <button id="seekForwardButton"></button>
    <button id="seekBackwardButton"></button>
    <strong><button id="fullscreenButton"></button></strong>
    <div id="videoCurrentTime"></div>
    <div id="videoDuration"></div>
    <div id="videoProgressBar"></div>
  </div>
</div>
fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
  }
});

function requestFullscreenVideo() {
  if (videoContainer.requestFullscreen) {
    videoContainer.requestFullscreen();
  } else {
    video.webkitEnterFullscreen();
  }
}

document.addEventListener('fullscreenchange', function () {
  fullscreenButton.classList.toggle('active', document.fullscreenElement);
});

Activer/Désactiver le plein écran en cas de changement d'orientation de l'écran

Lorsque l'utilisateur fait pivoter l'appareil en mode paysage, soyons intelligents et demandons automatiquement le plein écran pour créer une expérience immersive. Pour cela, nous avons besoin de l'API Screen Orientation, qui n'est pas encore disponible partout et qui est encore précédée d'un préfixe dans certains navigateurs. Il s'agit donc de notre première amélioration progressive.

Comment ça marche ? Dès que nous détectons que l'orientation de l'écran change, demandons le plein écran si la fenêtre du navigateur est en mode paysage (c'est-à-dire que sa largeur est supérieure à sa hauteur). Sinon, quittons le mode plein écran. C'est tout.

if ('orientation' in screen) {
  screen.orientation.addEventListener('change', function () {
    // Let's request fullscreen if user switches device in landscape mode.
    if (screen.orientation.type.startsWith('landscape')) {
      requestFullscreenVideo();
    } else if (document.fullscreenElement) {
      document.exitFullscreen();
    }
  });
}

Écran de verrouillage en mode Paysage en cliquant sur un bouton

Étant donné que la vidéo peut être mieux regardée en mode Paysage, il peut être judicieux de verrouiller l'écran en mode paysage lorsque l'utilisateur clique sur le bouton "Plein écran". Nous allons combiner l'API Screen Orientation précédemment utilisée et certaines requêtes multimédias pour nous assurer que cette expérience est la meilleure possible.

Verrouiller l'écran en mode paysage est aussi simple que d'appeler screen.orientation.lock('landscape'). Toutefois, nous ne devons le faire que lorsque l'appareil est en mode portrait avec matchMedia('(orientation: portrait)') et peut être tenu d'une seule main avec matchMedia('(max-device-width: 768px)'), car ce ne serait pas une expérience optimale pour les utilisateurs sur tablette.

fullscreenButton.addEventListener('click', function (event) {
  event.stopPropagation();
  if (document.fullscreenElement) {
    document.exitFullscreen();
  } else {
    requestFullscreenVideo();
    <strong>lockScreenInLandscape();</strong>;
  }
});
function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (
    matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches
  ) {
    screen.orientation.lock('landscape');
  }
}

Déverrouiller l'écran en cas de changement d'orientation de l'appareil

Vous avez peut-être remarqué que l'expérience de l'écran de verrouillage que nous venons de créer n'est pas parfaite, car nous ne recevons pas de modifications d'orientation de l'écran lorsque l'écran est verrouillé.

Pour résoudre ce problème, utilisez l'API Device Orientation, le cas échéant. Cette API fournit des informations à partir du matériel qui mesure la position et le mouvement d'un appareil dans l'espace : gyroscope et boussole numérique pour son orientation, et accéléromètre pour sa vitesse. Lorsque nous détectons un changement d'orientation de l'appareil, déverrouillons l'écran avec screen.orientation.unlock() si l'utilisateur tient l'appareil en mode portrait et que l'écran est verrouillé en mode paysage.

function lockScreenInLandscape() {
  if (!('orientation' in screen)) {
    return;
  }
  // Let's force landscape mode only if device is in portrait mode and can be held in one hand.
  if (matchMedia('(orientation: portrait) and (max-device-width: 768px)').matches) {
    screen.orientation.lock('landscape')
    <strong>.then(function() {
      listenToDeviceOrientationChanges();
    })</strong>;
  }
}
function listenToDeviceOrientationChanges() {
  if (!('DeviceOrientationEvent' in window)) {
    return;
  }
  var previousDeviceOrientation, currentDeviceOrientation;
  window.addEventListener(
    'deviceorientation',
    function onDeviceOrientationChange(event) {
      // event.beta represents a front to back motion of the device and
      // event.gamma a left to right motion.
      if (Math.abs(event.gamma) > 10 || Math.abs(event.beta) < 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        currentDeviceOrientation = 'landscape';
        return;
      }
      if (Math.abs(event.gamma) < 10 || Math.abs(event.beta) > 10) {
        previousDeviceOrientation = currentDeviceOrientation;
        // When device is rotated back to portrait, let's unlock screen orientation.
        if (previousDeviceOrientation == 'landscape') {
          screen.orientation.unlock();
          window.removeEventListener(
            'deviceorientation',
            onDeviceOrientationChange,
          );
        }
      }
    },
  );
}

Comme vous pouvez le constater, il s'agit de l'expérience en plein écran fluide que nous recherchions. Pour voir comment cela fonctionne, consultez l'exemple.

Lecture en arrière-plan

Lorsque vous constatez qu'une page Web ou une vidéo sur une page Web n'est plus visible, vous pouvez mettre à jour vos données analytiques pour le refléter. Cela peut également affecter la lecture en cours, comme choisir une autre piste, la mettre en pause ou même présenter des boutons personnalisés à l'utilisateur, par exemple.

Mettre en pause la vidéo en cas de modification de la visibilité de la page

L'API Page Visibility nous permet de déterminer la visibilité actuelle d'une page et de recevoir des notifications en cas de modification de la visibilité. Le code ci-dessous met la vidéo en pause lorsque la page est masquée. Cela se produit lorsque le verrouillage de l'écran est activé ou lorsque vous passez d'un onglet à l'autre, par exemple.

Étant donné que la plupart des navigateurs mobiles proposent désormais des commandes en dehors du navigateur qui permettent de reprendre une vidéo mise en pause, je vous recommande de ne définir ce comportement que si l'utilisateur est autorisé à lire une vidéo en arrière-plan.

document.addEventListener('visibilitychange', function () {
  // Pause video when page is hidden.
  if (document.hidden) {
    video.pause();
  }
});

Afficher/Masquer le bouton de coupure du son lors de la modification de la visibilité de la vidéo

Si vous utilisez la nouvelle API Intersection Observer, vous pouvez être encore plus précis sans frais. Cette API vous indique quand un élément observé entre ou sort de la fenêtre d'affichage du navigateur.

Affichons/Masquons un bouton de désactivation du son en fonction de la visibilité de la vidéo sur la page. Si la vidéo est en cours de lecture, mais qu'elle n'est pas visible, un mini bouton de mise en sourdine s'affiche en bas à droite de la page pour permettre à l'utilisateur de contrôler le son de la vidéo. L'événement vidéo volumechange permet de mettre à jour le style du bouton de coupure du son.

<button id="muteButton"></button>
if ('IntersectionObserver' in window) {
  // Show/hide mute button based on video visibility in the page.
  function onIntersection(entries) {
    entries.forEach(function (entry) {
      muteButton.hidden = video.paused || entry.isIntersecting;
    });
  }
  var observer = new IntersectionObserver(onIntersection);
  observer.observe(video);
}

muteButton.addEventListener('click', function () {
  // Mute/unmute video on button click.
  video.muted = !video.muted;
});

video.addEventListener('volumechange', function () {
  muteButton.classList.toggle('active', video.muted);
});

Lire une seule vidéo à la fois

Si une page comporte plusieurs vidéos, je vous suggère de n'en lire qu'une et de mettre les autres en pause automatiquement afin que l'utilisateur n'ait pas à entendre plusieurs pistes audio en même temps.

// This array should be initialized once all videos have been added.
var videos = Array.from(document.querySelectorAll('video'));

videos.forEach(function (video) {
  video.addEventListener('play', pauseOtherVideosPlaying);
});

function pauseOtherVideosPlaying(event) {
  var videosToPause = videos.filter(function (video) {
    return !video.paused && video != event.target;
  });
  // Pause all other videos currently playing.
  videosToPause.forEach(function (video) {
    video.pause();
  });
}

Personnaliser les notifications multimédias

Avec l'API Media Session, vous pouvez également personnaliser les notifications multimédias en fournissant des métadonnées pour la vidéo en cours de lecture. Il vous permet également de gérer les événements liés aux contenus multimédias, tels que la recherche ou le changement de piste, qui peuvent provenir de notifications ou de touches multimédias. Pour voir un exemple concret, consultez l'exemple.

Lorsque votre application Web lit du contenu audio ou vidéo, une notification multimédia s'affiche déjà dans la barre des notifications. Sur Android, Chrome fait de son mieux pour afficher les informations appropriées à l'aide du titre du document et de l'image d'icône la plus grande qu'il peut trouver.

Voyons comment personnaliser cette notification multimédia en définissant des métadonnées de session multimédia telles que le titre, l'artiste, le nom de l'album et l'illustration avec l'API Media Session.

playPauseButton.addEventListener('click', function(event) {
  event.stopPropagation();
  if (video.paused) {
    video.play()
    <strong>.then(function() {
      setMediaSession();
    });</strong>
  } else {
    video.pause();
  }
});
function setMediaSession() {
  if (!('mediaSession' in navigator)) {
    return;
  }
  navigator.mediaSession.metadata = new MediaMetadata({
    title: 'Never Gonna Give You Up',
    artist: 'Rick Astley',
    album: 'Whenever You Need Somebody',
    artwork: [
      {src: 'https://dummyimage.com/96x96', sizes: '96x96', type: 'image/png'},
      {
        src: 'https://dummyimage.com/128x128',
        sizes: '128x128',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/192x192',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/256x256',
        sizes: '256x256',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/384x384',
        sizes: '384x384',
        type: 'image/png',
      },
      {
        src: 'https://dummyimage.com/512x512',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
  });
}

Une fois la lecture terminée, vous n'avez pas besoin de "libérer" la session multimédia, car la notification disparaît automatiquement. N'oubliez pas que l'navigator.mediaSession.metadata actuelle sera utilisée au début de la lecture. C'est pourquoi vous devez le mettre à jour pour vous assurer de toujours afficher des informations pertinentes dans la notification multimédia.

Si votre application Web fournit une playlist, vous pouvez autoriser l'utilisateur à la parcourir directement depuis la notification multimédia avec des icônes "Piste précédente" et "Piste suivante".

if ('mediaSession' in navigator) {
  navigator.mediaSession.setActionHandler('previoustrack', function () {
    // User clicked "Previous Track" media notification icon.
    playPreviousVideo(); // load and play previous video
  });
  navigator.mediaSession.setActionHandler('nexttrack', function () {
    // User clicked "Next Track" media notification icon.
    playNextVideo(); // load and play next video
  });
}

Notez que les gestionnaires d'actions multimédias seront conservés. Ce modèle est très semblable au modèle d'écouteur d'événements, à la différence que la gestion d'un événement signifie que le navigateur cesse d'effectuer tout comportement par défaut et l'utilise comme un signal indiquant que votre application Web prend en charge l'action multimédia. Par conséquent, les commandes d'action multimédias ne s'affichent que si vous définissez le gestionnaire d'action approprié.

Par ailleurs, désactiver un gestionnaire d'action multimédia est aussi simple que de l'attribuer à null.

L'API Media Session vous permet d'afficher les icônes de notification multimédia "Rechercher en arrière" et "Rechercher en avant" si vous souhaitez contrôler la durée de la lecture à sauter.

if ('mediaSession' in navigator) {
  let skipTime = 10; // Time to skip in seconds

  navigator.mediaSession.setActionHandler('seekbackward', function () {
    // User clicked "Seek Backward" media notification icon.
    video.currentTime = Math.max(video.currentTime - skipTime, 0);
  });
  navigator.mediaSession.setActionHandler('seekforward', function () {
    // User clicked "Seek Forward" media notification icon.
    video.currentTime = Math.min(video.currentTime + skipTime, video.duration);
  });
}

L'icône "Lecture/Pause" s'affiche toujours dans la notification multimédia, et les événements associés sont gérés automatiquement par le navigateur. Si, pour une raison quelconque, le comportement par défaut ne fonctionne pas, vous pouvez toujours gérer les événements multimédias "Lecture" et "Pause".

L'avantage de l'API Media Session est que la barre de notification n'est pas le seul endroit où les métadonnées et les commandes multimédias sont visibles. La notification multimédia est synchronisée automatiquement avec tous les appareils connectés. Et il apparaît également sur les écrans de verrouillage.

Commentaires