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 utilisateur et de l'importance que vous accordez aux contenus multimédias sur une page Web. Nous sommes tous d'accord pour dire que si la vidéo est LA raison de la visite d'un utilisateur, son expérience doit être immersive et l'inciter à revenir.

lecture de vidéos sur le Web mobile

Dans cet article, je vous explique 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, le plein écran et la lecture en arrière-plan. Vous pouvez essayer l'exemple dès maintenant 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 aborderons plus tard incluent un bouton de lecture/pause, un bouton 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 de la vidéo

Commençons par attendre que les métadonnées de la 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 et qui convertit un nombre de secondes en une 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éos 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 qui permet à 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. Le fait de baser nos commandes sur des événements améliore la flexibilité (comme nous le verrons plus tard avec l'API Media Session) et nous permettra de maintenir la synchronisation de nos commandes si le navigateur intervient dans la lecture. Lorsque la lecture de la vidéo commence, nous changeons l'état du bouton en "pause" et masquons les commandes vidéo. Lorsque la vidéo est mise en pause, nous changeons simplement l'état du bouton en "lecture" 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 est modifiée 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 "play", nous remettons la propriété currentTime de la vidéo à 0 et nous affichons les commandes de la vidéo pour le moment. Notez que nous pourrions é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;
});

Revenir en arrière et avancer

Continuons et ajoutons des boutons "reculer " et"avancer " pour 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 plein écran.

Plein écran

Ici, nous allons utiliser plusieurs API Web pour créer une expérience plein écran parfaite et fluide. Pour voir une démonstration, consultez l'exemple.

Bien sûr, vous n'êtes pas obligé de tous les utiliser. Il vous suffit de choisir ceux 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 du contenu multimédia commence. Comme nous essayons de personnaliser et de contrôler autant que possible notre expérience multimédia sur les navigateurs mobiles, je vous recommande de définir l'attribut playsinline de l'élément video pour forcer la lecture intégrée 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 le 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", nous 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 avec la méthode requestFullscreen() si elle est disponible, ou utilisez webkitEnterFullscreen() sur 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 mode plein écran lorsque l'orientation de l'écran change

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 aurons besoin de l'API Screen Orientation, qui n'est pas encore compatible partout et qui est toujours préfixée dans certains navigateurs à ce moment-là. Il s'agira donc de notre première amélioration progressive.

Fonctionnement Dès que nous détectons un changement d'orientation de l'écran, 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). Si ce n'est pas le cas, quittez 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();
    }
  });
}

Verrouiller l'écran en mode paysage en cliquant sur un bouton

Comme les vidéos sont plus faciles à regarder en mode Paysage, nous pouvons verrouiller l'écran en mode Paysage lorsque l'utilisateur clique sur le bouton "Plein écran". Nous allons combiner l'API Screen Orientation utilisée précédemment et certaines requêtes média pour garantir une expérience optimale.

Pour verrouiller l'écran en mode paysage, il suffit 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 l'expérience ne serait pas optimale pour les utilisateurs de tablettes.

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 lors d'un changement d'orientation de l'appareil

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

Pour résoudre ce problème, utilisons l'API Device Orientation si elle est disponible. Cette API fournit des informations provenant du matériel qui mesure la position et le mouvement d'un appareil dans l'espace : le gyroscope et la boussole numérique pour son orientation, et l'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 voir, nous avons obtenu l'expérience plein écran fluide que nous recherchions. Pour voir une démonstration, consultez l'exemple.

Lecture en arrière-plan

Lorsque vous détectez qu'une page Web ou une vidéo de la page Web n'est plus visible, vous pouvez mettre à jour vos données analytiques pour en tenir compte. Cela peut également affecter la lecture en cours, par exemple en sélectionnant une autre piste, en la mettant en pause ou même en affichant des boutons personnalisés pour l'utilisateur.

Mettre la vidéo en pause lorsque la visibilité de la page change

L'API Page Visibility nous permet de déterminer la visibilité actuelle d'une page et d'être informés des changements de visibilité. Le code ci-dessous met en pause la vidéo lorsque la page est masquée. Cela se produit lorsque le verrouillage de l'écran est actif ou lorsque vous changez d'onglet, par exemple.

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 donc de ne définir ce comportement que si l'utilisateur est autorisé à lire la 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 permettant de couper le son lorsque la visibilité de la vidéo change

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

Affichons/Masquons un bouton de coupure du son en fonction de la visibilité de la vidéo sur la page. Si la vidéo est en cours de lecture, mais n'est pas visible, un petit bouton de coupure du son 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 modifier le style du bouton du micro.

<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 contient plusieurs vidéos, nous vous suggérons de n'en lire qu'une seule et de mettre automatiquement les autres en pause pour 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

L'API Media Session vous permet également de personnaliser les notifications multimédias en fournissant des métadonnées pour la vidéo en cours de lecture. Elle vous permet également de gérer les événements liés aux contenus multimédias, tels que la recherche ou le changement de titre, qui peuvent provenir de notifications ou de touches multimédias. Pour voir une démonstration, 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 de notification. Sur Android, Chrome fait de son mieux pour afficher les informations appropriées en utilisant le titre du document et l'image d'icône la plus grande qu'il peut trouver.

Voyons comment personnaliser cette notification multimédia en définissant certaines métadonnées de session multimédia, telles que le titre, l'artiste, le nom de l'album et l'illustration, à l'aide de 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 le navigator.mediaSession.metadata actuel sera utilisé au début de chaque lecture. C'est pourquoi vous devez la 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 à parcourir votre playlist directement à partir de la notification multimédia à l'aide des icônes "Titre précédent" et "Titre suivant".

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 à celui de l'écouteur d'événements, sauf que la gestion d'un événement signifie que le navigateur cesse d'adopter un comportement par défaut et l'utilise comme un signal indiquant que votre application Web est compatible avec l'action multimédia. Par conséquent, les commandes d'action multimédia ne s'affichent que si vous définissez le gestionnaire d'action approprié.

Pour annuler un gestionnaire d'actions multimédias, il vous suffit de lui attribuer la valeur null.

L'API Media Session vous permet d'afficher les icônes de notification multimédia "Reculer" et "Avancer" si vous souhaitez contrôler la durée de l'avance ou du retour rapide.

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" est toujours affichée 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 tout accessoire connecté associé. et s'affiche également sur les écrans de verrouillage.

Commentaires