Lecture rapide avec préchargement audio et vidéo

Découvrez comment accélérer la lecture de vos contenus multimédias en préchargeant activement les ressources.

François Beaufort
François Beaufort

Un démarrage plus rapide signifie que davantage de personnes regardent votre vidéo ou écoutent votre audio. C'est un fait connu. Dans cet article, nous aborderons que vous pouvez utiliser pour accélérer la lecture de vos contenus audio et vidéo en les ressources selon votre cas d'utilisation.

Crédits: copyright Blender Foundation | www.blender.org .

Je vais décrire trois méthodes de préchargement des fichiers multimédias, en commençant par les pros et ses inconvénients.

C'est génial... Mais…
Attribut de préchargement de la vidéo Simple à utiliser pour un fichier unique hébergé sur un serveur Web. Les navigateurs peuvent ignorer complètement l'attribut.
La récupération des ressources commence lorsque le document HTML a été entièrement chargé et analysées.
Les extensions de source multimédia (MSE) ignorent l'attribut preload sur les éléments multimédias, car l'application est responsable de la fourniture de médias à MSE.
Préchargement du lien Force le navigateur à demander une ressource vidéo sans blocage l'événement onload du document. Les requêtes HTTP Range ne sont pas compatibles.
Compatible avec MSE et les segments de fichiers. À utiliser uniquement pour les petits fichiers multimédias (< 5 Mo) lors de l'extraction de ressources complètes.
Mise en mémoire tampon manuelle Contrôle complet La gestion des erreurs complexes relève de la responsabilité du site Web.

Attribut de préchargement de la vidéo

Si la source vidéo est un fichier unique hébergé sur un serveur Web, vous pouvez utilisez l'attribut vidéo preload pour indiquer au navigateur comment trop d'informations ou de contenu à précharger Autrement dit, les extensions de source multimédia (MSE) n'est pas compatible avec preload.

L'extraction des ressources ne démarre que lorsque le document HTML initial a été chargé et analysé (par exemple, l'événement DOMContentLoaded s'est déclenché). tandis que l'événement load très différent est déclenché a bien été récupérée.

Définir l'attribut preload sur metadata indique que l'utilisateur n'est pas auraient besoin de la vidéo, mais que l'extraction de ses métadonnées (dimensions, piste la liste, la durée, etc.) est souhaitable. Notez qu'à partir de Chrome 64, la valeur par défaut de preload est metadata. (Il était auto précédemment).

<video id="video" preload="metadata" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Définir l'attribut preload sur auto indique que le navigateur peut mettre en cache de données suffisantes pour que la lecture soit complète sans nécessiter d'arrêt pour mise en mémoire tampon.

<video id="video" preload="auto" src="file.mp4" controls></video>

<script>
  video.addEventListener('loadedmetadata', function() {
    if (video.buffered.length === 0) return;

    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);
  });
</script>

Il y a cependant quelques mises en garde. Comme ceci n'est qu'un indice, le navigateur peut avoir totalement ignore l'attribut preload. Au moment de la rédaction de ce document, voici quelques règles appliquées dans Chrome:

  • Lorsque l'économiseur de données est activé, Chrome force la valeur preload à none
  • Dans Android 4.3, Chrome force la valeur preload à none en raison d'une erreur Android Bug.
  • Avec une connexion au réseau mobile (2G, 3G ou 4G), Chrome force la valeur preload à metadata

Conseils

Si votre site Web contient de nombreuses ressources vidéo sur le même domaine, nous vous recommandons de définir la valeur preload sur metadata ou de définir l'poster et définissez preload sur none. De cette façon, vous éviteriez le nombre maximal de connexions HTTP vers le même domaine (six selon le spécification HTTP 1.1), ce qui peut bloquer le chargement des ressources. Notez que cela peut aussi améliorer la vitesse des pages si les vidéos ne font pas partie de l'expérience utilisateur principale.

Comme décrit dans d'autres articles, le préchargement des liens est une extraction déclarative qui vous permet de forcer le navigateur à envoyer une requête pour une ressource bloquer l'événement load et pendant le téléchargement de la page. Ressources chargés via <link rel="preload"> sont stockés localement dans le navigateur et sont efficacement inertes jusqu'à ce qu'elles soient explicitement référencées dans le DOM, JavaScript, ou CSS.

Le préchargement est différent du préchargement en ce qu'il se concentre sur la navigation actuelle et extrait les ressources en fonction de leur type (script, style, police, vidéo, audio, etc.). Il doit servir à préchauffer le cache du navigateur pour l'état actuel sessions.

Précharger la vidéo complète

Voici comment précharger une vidéo complète sur votre site Web afin que, lorsque votre JavaScript demande l'extraction d'un contenu vidéo. Celui-ci est lu dans le cache en tant que ressource ont peut-être déjà été mis en cache par le navigateur. Si la demande de préchargement n'a pas été terminé, une récupération réseau régulière aura lieu.

<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">

<video id="video" controls></video>

<script>
  // Later on, after some condition has been met, set video source to the
  // preloaded video URL.
  video.src = 'https://cdn.com/small-file.mp4';
  video.play().then(() => {
    // If preloaded video URL was already cached, playback started immediately.
  });
</script>

Comme la ressource préchargée sera utilisée par un élément vidéo dans Dans cet exemple, la valeur du lien de préchargement as est video. S'il s'agissait d'une piste audio il s'agit de as="audio".

Précharger le premier segment

L'exemple ci-dessous montre comment précharger le premier segment d'une vidéo avec <link rel="preload"> et comment l'utiliser avec les extensions de source multimédia. Si vous ne connaissez pas avec l'API MSE JavaScript, consultez la page Principes de base de MSE.

Par souci de simplicité, supposons que l'intégralité de la vidéo a été divisée en des fichiers plus petits comme file_1.webm, file_2.webm, file_3.webm, etc.

<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // If video is preloaded already, fetch will return immediately a response
    // from the browser cache (memory cache). Otherwise, it will perform a
    // regular network fetch.
    fetch('https://cdn.com/file_1.webm')
    .then(response => response.arrayBuffer())
    .then(data => {
      // Append the data into the new sourceBuffer.
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch file_2.webm when user starts playing video.
    })
    .catch(error => {
      // TODO: Show "Video is not available" message to user.
    });
  }
</script>

Assistance

Vous pouvez détecter la prise en charge de différents types de as pour <link rel=preload> à l'aide de extraits ci-dessous:

function preloadFullVideoSupported() {
  const link = document.createElement('link');
  link.as = 'video';
  return (link.as === 'video');
}

function preloadFirstSegmentSupported() {
  const link = document.createElement('link');
  link.as = 'fetch';
  return (link.as === 'fetch');
}

Mise en mémoire tampon manuelle

Avant de nous pencher sur l'API Cache et les service workers, voyons comment mettre une vidéo en mémoire tampon manuellement avec MSE. L'exemple ci-dessous part du principe que votre site serveur est compatible avec HTTP Range mais ce serait assez similaire avec segments. Notez que certaines bibliothèques de middleware telles que Shaka de Google Player, JW Player et Video.js sont conçue pour gérer cela pour vous.

<video id="video" controls></video>

<script>
  const mediaSource = new MediaSource();
  video.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

  function sourceOpen() {
    URL.revokeObjectURL(video.src);
    const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');

    // Fetch beginning of the video by setting the Range HTTP request header.
    fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      sourceBuffer.appendBuffer(data);
      sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
    });
  }

  function updateEnd() {
    // Video is now ready to play!
    const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
    console.log(`${bufferedSeconds} seconds of video are ready to play.`);

    // Fetch the next segment of video when user starts playing the video.
    video.addEventListener('playing', fetchNextSegment, { once: true });
  }

  function fetchNextSegment() {
    fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
    .then(response => response.arrayBuffer())
    .then(data => {
      const sourceBuffer = mediaSource.sourceBuffers[0];
      sourceBuffer.appendBuffer(data);
      // TODO: Fetch further segment and append it.
    });
  }
</script>

Remarques

Comme vous contrôlez désormais l'intégralité de la mise en mémoire tampon du contenu multimédia, je vous recommande le niveau de batterie de l'appareil, le mode Économiseur de données, les préférences de l'utilisateur et des informations sur le réseau quand on parle de préchargement.

Détection de la batterie

Tenez compte du niveau de batterie des utilisateurs appareils avant de réfléchir sur le préchargement d'une vidéo. Cela permettra de préserver l'autonomie de la batterie lorsque le niveau de charge est faible.

Désactivez le préchargement ou au moins préchargez une vidéo de résolution inférieure lorsque la appareil est à court de batterie.

if ('getBattery' in navigator) {
  navigator.getBattery()
  .then(battery => {
    // If battery is charging or battery level is high enough
    if (battery.charging || battery.level > 0.15) {
      // TODO: Preload the first segment of a video.
    }
  });
}

Détecter "Data-Saver"

Utilisez l'en-tête de requête d'optimisation du client Save-Data pour une diffusion rapide et légère. applications auprès des utilisateurs ayant activé le "Économie de données" dans leur navigateur. En identifiant cet en-tête de requête, votre application peut personnaliser et offrir une expérience utilisateur optimisée pour les entreprises aux utilisateurs.

Pour en savoir plus, consultez la page Proposer des applications rapides et légères avec Save-Data.

Chargement intelligent basé sur les informations réseau

Nous vous conseillons de vérifier navigator.connection.type avant le préchargement. Quand ? si elle est définie sur cellular, vous pouvez empêcher le préchargement et indiquer aux utilisateurs que leur opérateur de réseau mobile facture pour la bande passante et ne fait que commencer la lecture automatique du contenu précédemment mis en cache ;

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Consultez l'exemple "Informations sur le réseau" pour savoir comment réagir aux changent également.

Mettre en pré-cache plusieurs premiers segments

Comment effectuer un préchargement spéculatif de contenu multimédia de savoir quel média l'utilisateur va finalement choisir ? Si l'utilisateur se trouve sur un contenant 10 vidéos, nous avons probablement assez de mémoire pour en récupérer une. de chaque fichier d'analyse, mais nous ne devrions pas créer 10 <video> et 10 objets MediaSource, puis commencez à alimenter ces données.

L'exemple en deux parties ci-dessous montre comment mettre en pré-cache plusieurs premiers segments de vidéo à l'aide de l'API Cache, puissante et simple d'utilisation. Notez que quelque chose de similaire peut aussi être réalisé avec IndexedDB. Nous n'utilisons pas encore de service workers, l'API Cache est également accessible depuis l'objet window.

Extraire et mettre en cache

const videoFileUrls = [
  'bat_video_file_1.webm',
  'cow_video_file_1.webm',
  'dog_video_file_1.webm',
  'fox_video_file_1.webm',
];

// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));

function fetchAndCache(videoFileUrl, cache) {
  // Check first if video is in the cache.
  return cache.match(videoFileUrl)
  .then(cacheResponse => {
    // Let's return cached response if video is already in the cache.
    if (cacheResponse) {
      return cacheResponse;
    }
    // Otherwise, fetch the video from the network.
    return fetch(videoFileUrl)
    .then(networkResponse => {
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, networkResponse.clone());
      return networkResponse;
    });
  });
}

Notez que si je devais utiliser des requêtes HTTP Range, je devrai recréer manuellement Un objet Response, car l'API Cache n'accepte pas encore les réponses Range. Être gardez à l'esprit que l'appel de networkResponse.arrayBuffer() permet de récupérer l'intégralité du contenu de la réponse en une seule fois dans la mémoire du moteur de rendu. de petites plages.

Pour référence, j'ai modifié une partie de l'exemple ci-dessus pour enregistrer la plage HTTP les demandes de mise en cache vidéo.

    ...
    return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
    .then(networkResponse => networkResponse.arrayBuffer())
    .then(data => {
      const response = new Response(data);
      // Add the response to the cache and return network response in parallel.
      cache.put(videoFileUrl, response.clone());
      return response;
    });

Lire la vidéo

Lorsqu'un utilisateur clique sur un bouton de lecture, nous récupérons le premier segment de la vidéo dans l'API Cache pour que la lecture démarre immédiatement si elle est disponible. Sinon, nous allons simplement le récupérer sur le réseau. N'oubliez pas que les navigateurs et les utilisateurs peuvent décider de vider le cache.

Comme nous l'avons vu précédemment, nous utilisons la MSE pour ajouter ce premier segment de la vidéo à la vidéo. .

function onPlayButtonClick(videoFileUrl) {
  video.load(); // Used to be able to play video later.

  window.caches.open('video-pre-cache')
  .then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
  .then(response => response.arrayBuffer())
  .then(data => {
    const mediaSource = new MediaSource();
    video.src = URL.createObjectURL(mediaSource);
    mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });

    function sourceOpen() {
      URL.revokeObjectURL(video.src);

      const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
      sourceBuffer.appendBuffer(data);

      video.play().then(() => {
        // TODO: Fetch the rest of the video when user starts playing video.
      });
    }
  });
}

Créer des réponses Range avec un service worker

Que se passe-t-il si vous avez récupéré l'intégralité d'un fichier vidéo et l'avez enregistré l'API Cache ? Lorsque le navigateur envoie une requête HTTP Range, vous n'avez absolument pas car l'API Cache ne permet pas de stocker l'intégralité de la vidéo sont encore compatibles avec les réponses Range.

Voyons comment intercepter ces requêtes et renvoyer un Range personnalisé. d'un service worker.

addEventListener('fetch', event => {
  event.respondWith(loadFromCacheOrFetch(event.request));
});

function loadFromCacheOrFetch(request) {
  // Search through all available caches for this request.
  return caches.match(request)
  .then(response => {

    // Fetch from network if it's not already in the cache.
    if (!response) {
      return fetch(request);
      // Note that we may want to add the response to the cache and return
      // network response in parallel as well.
    }

    // Browser sends a HTTP Range request. Let's provide one reconstructed
    // manually from the cache.
    if (request.headers.has('range')) {
      return response.blob()
      .then(data => {

        // Get start position from Range request header.
        const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
        const options = {
          status: 206,
          statusText: 'Partial Content',
          headers: response.headers
        }
        const slicedResponse = new Response(data.slice(pos), options);
        slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
            (data.size - 1) + '/' + data.size);
        slicedResponse.setHeaders('X-From-Cache': 'true');

        return slicedResponse;
      });
    }

    return response;
  }
}

Il est important de noter que j'ai utilisé response.blob() pour recréer ce segment car cela me donne simplement un handle vers le fichier tout en response.arrayBuffer() ajoute le fichier entier dans la mémoire du moteur de rendu.

Je peux utiliser mon en-tête HTTP X-From-Cache personnalisé pour savoir si cette requête provenaient du cache ou du réseau. Il peut être utilisé par un lecteur ShakaPlayer pour ignorer le temps de réponse en tant qu'indicateur la vitesse du réseau.

Découvrez l'application Sample Media App officielle et en particulier ses ranged-response.js pour obtenir une solution complète sur la gestion de Range requêtes.