Lecture de vidéos sur le Web mobile

François Beaufort
François Beaufort

Comment proposer 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 médias sur une page Web. Je pense que nous sommes tous d'accord pour dire que si la vidéo est la raison pour laquelle un utilisateur se rend sur le site, l'expérience doit être immersive et stimulante.

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, ainsi que la lecture en plein écran et 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 lecture/pause, un bouton plein écran, des boutons de retour à la page précédente et d'avance, ainsi que certains éléments pour le suivi de l'heure actuelle, de la durée et de la durée.

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

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

Attendez que les métadonnées de la vidéo soient chargées pour définir sa durée et 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 chaîne au format "hh:mm:ss", ce qui convient mieux à 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 vidéo uniquement
Figure 2. Lecteur multimédia affichant les métadonnées vidéo

Lire/Mettre en pause la vidéo

Maintenant que les métadonnées vidéo sont chargées, ajoutons le 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 événements de commandes offre davantage de flexibilité (comme nous le verrons plus tard avec l'API Media Session) et nous permettra de synchroniser nos commandes si le navigateur intervient dans la lecture. Lorsque la lecture de la vidéo commence, nous modifions l'état du bouton pour le mettre en pause et nous masquons les commandes vidéo. Lorsque la vidéo est mise en pause, il suffit de remplacer l'état du bouton par "Lecture" et d'afficher 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 vidéo currentTime 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
    })`;
  }
});

À la fin de la vidéo, il nous suffit de remplacer l'état du bouton par "Lecture", de rétablir le paramètre currentTime de la vidéo sur 0 et d'afficher les commandes 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;
});

Avancer et reculer

Continuons en ajoutant des boutons "Reculer vers l'arrière" et "Avancer" 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, au lieu 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');
});

Vous trouverez ci-dessous ce que nous avons créé jusqu'à présent. Dans la section suivante, nous allons implémenter le bouton plein écran.

Plein écran

Dans cet exemple, nous allons exploiter plusieurs API Web pour créer une expérience plein écran parfaite et fluide. Pour la voir en action, consultez l'exemple.

Évidemment, vous n'êtes pas obligé de tous les utiliser. Choisissez simplement celles qui ont du sens pour vous et combinez-les pour créer votre flux personnalisé.

Empêcher le plein écran automatique

Sur iOS, les éléments video passent automatiquement en mode plein écran au début de la lecture de contenus multimédias. Comme nous essayons d'adapter et de contrôler le plus possible notre expérience multimédia sur les navigateurs mobiles, nous vous recommandons de définir l'attribut playsinline de l'élément video pour forcer sa lecture intégrée sur l'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 mode plein écran lorsque l'utilisateur clique sur le bouton

Maintenant que nous empêchons le plein écran automatique, nous devons gérer nous-mêmes le mode plein écran pour la vidéo avec l'API plein écran. 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 mode 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 changement d'orientation du mode plein écran à l'écran

Alors que l'utilisateur fait pivoter l'appareil en mode Paysage, soyez prudent et demandez automatiquement le plein écran pour créer une expérience immersive. Pour ce faire, nous avons besoin de l'API Screen Orientation, qui n'est pas encore prise en charge partout et qui est encore ajoutée comme préfixe dans certains navigateurs à ce moment-là. 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 l'affichage en plein écran si la fenêtre du navigateur est en mode Paysage (c'est-à-dire si sa largeur est supérieure à sa hauteur). Sinon, 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();
    }
  });
}

Écran de verrouillage en mode paysage lorsque l'utilisateur clique sur un bouton

Étant donné que la vidéo est peut-être mieux visionnée en mode Paysage, il peut être utile 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 garantir la meilleure expérience possible.

Pour verrouiller l'écran en mode paysage, il vous suffit d'appeler screen.orientation.lock('landscape'). Toutefois, nous ne devons effectuer cette opération que lorsque l'appareil est en mode Portrait avec matchMedia('(orientation: portrait)') et qu'il peut être tenu dans une main avec matchMedia('(max-device-width: 768px)'), car cela 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 lors du 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 de changement d'orientation de l'écran lorsque celui-ci est verrouillé.

Pour résoudre ce problème, utilisons l'API Device Orientation, si 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éverrouillez 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, il s'agit de l'expérience plein écran fluide que nous recherchions. Pour voir un exemple concret, consultez l'exemple.

Lecture en arrière-plan

Lorsque vous détectez qu'une page Web ou qu'une vidéo n'y est plus visible, vous pouvez mettre à jour vos données analytiques en conséquence. Cela peut également affecter la lecture en cours, comme le choix d'une autre piste, la mise en pause ou même l'affichage de boutons personnalisés à l'utilisateur, par exemple.

Modification de la visibilité de la vidéo sur la mise en pause

L'API Page Visibility nous permet de déterminer la visibilité actuelle d'une page et d'être informé 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 activé ou lorsque vous changez d'onglet par exemple.

Étant donné que la plupart des navigateurs mobiles proposent désormais des commandes en dehors du navigateur permettant de reprendre une vidéo mise en pause, nous vous recommandons 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 du changement de visibilité d'une vidéo

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 qu'elle n'est pas visible actuellement, un mini-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 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

S'il y a plusieurs vidéos sur une page, je vous recommande de n'en lire qu'une seule 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 des événements multimédias, tels que la recherche ou le changement de suivi, qui peuvent provenir de notifications ou de touches multimédias. Pour voir comment cela fonctionne, 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 s'efforce d'afficher les informations appropriées en utilisant le titre du document et la plus grande image d'icône disponible.

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 la pochette à 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îtra automatiquement. N'oubliez pas que l'élément navigator.mediaSession.metadata actuel est utilisé 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 à 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 sont 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'exécuter tout comportement par défaut et l'utilise comme signal indiquant que votre application Web prend en charge l'action multimédia. Par conséquent, les commandes des actions multimédias ne s'affichent que si vous définissez le gestionnaire d'actions approprié.

Il est aussi facile de désactiver un gestionnaire d'actions multimédias que de l'attribuer à null.

L'API Media Session vous permet d'afficher les icônes de notification multimédia "Seek Backward" (Avancer) et "Seek Forward" (Avancer) si vous souhaitez contrôler le temps écoulé.

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ù sont visibles les commandes et les métadonnées multimédias. La notification multimédia est synchronisée automatiquement avec tout accessoire connecté associé. Et il s'affiche aussi sur les écrans de verrouillage.

Commentaires