Reproducción rápida con precarga de audio y video

Cómo acelerar la reproducción de contenido multimedia con la precarga activa de recursos

François Beaufort
François Beaufort

Un inicio de reproducción más rápido implica que más personas miran tu video o escuchan tu audio. Es un dato conocido. En este artículo, exploraré técnicas que puedes usar para acelerar la reproducción de audio y video mediante la precarga de recursos según tu caso de uso.

Créditos: Copyright Remixer Foundation | www.blender.org .

Describiré tres métodos de precarga de archivos multimedia, comenzando por los profesionales y desventajas.

Es genial... Pero…
Atributo de precarga de video Fácil de usar para un archivo único alojado en un servidor web. Es posible que los navegadores ignoren por completo este atributo.
La recuperación de recursos comienza cuando el documento HTML se cargó por completo. analizar.
Las extensiones de fuente de medios (MSE) ignoran el atributo preload en los elementos multimedia porque la app es responsable de proporcionando medios a ECM.
Carga previa del vínculo Fuerza al navegador a realizar una solicitud de un recurso de video sin bloquear. el evento onload del documento. Las solicitudes de rango HTTP no son compatibles.
Es compatible con ECM y segmentos de archivos. Se debe usar solo para archivos multimedia pequeños (<5 MB) cuando se recuperan recursos completos.
Almacenamiento en búfer manual Control total El manejo de errores complejos es responsabilidad del sitio web.

Atributo de precarga de video

Si la fuente del video es un archivo único alojado en un servidor web, te recomendamos Usa el atributo preload de video para sugerir al navegador una sugerencia sobre cómo mucha información o contenido que se precarga. Esto significa que las extensiones de fuente de medios (ECM) no es compatible con preload.

La recuperación de recursos comenzará solo después de que el documento HTML inicial se haya Se cargó y se analizó completamente (p. ej., se activó el evento DOMContentLoaded). mientras que el evento load, que es muy diferente, se activa cuando el recurso ya se recuperó.

Si estableces el atributo preload en metadata, esto indica que el usuario no está se espera que necesiten el video, sino que recuperar los metadatos (dimensiones, pistas lista, duración, etc.) es deseable. Ten en cuenta que, si comienzas en Chrome 64, el valor predeterminado para preload es metadata. (Era auto anteriormente).

<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>

Establecer el atributo preload en auto indica que el navegador puede almacenar en caché suficientes datos para completar la reproducción sin necesidad de detenerse almacenamiento en búfer adicional.

<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>

Sin embargo, hay algunas advertencias. Como esto es solo una pista, es posible que el navegador ignora el atributo preload. Hasta el momento de la redacción, estas son algunas reglas aplicadas en Chrome:

  • Cuando el Ahorro de datos está habilitado, Chrome fuerza el valor preload a none
  • En Android 4.3, Chrome fuerza el valor de preload a none debido a un error de Android Error.
  • En una conexión móvil (2G, 3G y 4G), Chrome fuerza el valor de preload a metadata

Sugerencias

Si tu sitio web contiene muchos recursos de video en el mismo dominio, se recomienda establecer el valor de preload en metadata o definir el poster y configura preload como none. De esa manera, evitas tocar la cantidad máxima de conexiones HTTP al mismo dominio (6 según el especificación de HTTP 1.1) que puede detener la carga de recursos. Ten en cuenta que esto también podría mejorar la velocidad de la página si los videos no son parte de la experiencia principal del usuario.

Como se trata en otros artículos, la precarga de vínculos es una recuperación declarativa que te permite forzar al navegador a realizar una solicitud de un recurso sin bloqueando el evento load y mientras se descarga la página. Recursos cargados a través de <link rel="preload"> se almacenan localmente en el navegador y se se mantienen inertes hasta que se haga una referencia explícita a ellas en el DOM, JavaScript o CSS.

La carga previa es diferente de la carga previa, ya que se enfoca en la navegación y recupera recursos con prioridad según su tipo (secuencia de comandos, estilo, fuente, video, audio, etc.). Debe usarse para preparar la caché del navegador para sesiones.

Precargar video completo

Aquí te mostramos cómo precargar un video completo en tu sitio web para que cuando JavaScript solicita recuperar el contenido de video; se lee desde la caché como recurso puede haber sido almacenado en caché por el navegador. Si la solicitud de precarga aún no haya finalizado, se realizará una recuperación de red normal.

<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>

Como el recurso precargado será consumido por un elemento de video en En el ejemplo, el valor del vínculo precargado as es video. Si se tratara de un audio elemento, sería as="audio".

Precarga el primer segmento

En el siguiente ejemplo, se muestra cómo precargar el primer segmento de un video con <link rel="preload"> y usarlo con extensiones de fuente de medios. Si desconocen con la API de JavaScript de ECM, consulta Conceptos básicos de ECM.

Para simplificar, supongamos que todo el video se dividió en archivos más pequeños, como 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>

Asistencia

Puedes detectar la compatibilidad de varios tipos de as para <link rel=preload> con el elemento algunos fragmentos a continuación:

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');
}

Almacenamiento en búfer manual

Antes de analizar la API de Cache y los service workers, veamos cómo almacenar manualmente un video en el búfer con ECM. En el siguiente ejemplo, se supone que tu sitio web servidor compatible con HTTP Range pero esto sería bastante similar con el caso de segmentos. Ten en cuenta que algunas bibliotecas de middleware, como Google's Shaka Player, JW Player y Video.js son creado para manejar esto por ti.

<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>

Consideraciones

Como tienes el control de toda la experiencia de almacenamiento en búfer de contenido multimedia, ten en cuenta el nivel de batería del dispositivo, el "modo de ahorro de datos" las preferencias del usuario y la información de la red cuando pienses en la precarga.

Reconocimiento de batería

Ten en cuenta el nivel de batería de las los dispositivos antes de pensar sobre la precarga de un video. Esto conservará la duración de la batería cuando el nivel de es bajo.

Inhabilita la precarga o al menos precargar un video de menor resolución cuando dispositivo se está quedando sin batería.

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.
    }
  });
}

Detectar "Ahorro de datos"

Usa el encabezado de solicitud de sugerencia de cliente Save-Data para realizar entregas rápidas y ligeras. aplicaciones a los usuarios que han optado por el "ahorro de datos" en su navegador. Al identificar este encabezado de la solicitud, tu aplicación puede personalizar y brindar una experiencia del usuario optimizada usuarios.

Consulta Cómo publicar aplicaciones rápidas y livianas con Save-Data para obtener más información.

Carga inteligente basada en la información de la red

Te recomendamos verificar navigator.connection.type antes de la precarga. Cuándo está configurado en cellular, puedes evitar la precarga y avisarles a los usuarios que es posible que su operador de red móvil esté cargando por el ancho de banda y solo comience reproducción automática de contenido previamente almacenado en caché

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

Consulta la muestra de información de red para saber cómo reaccionar ante la los cambios.

Almacena previamente en caché múltiples primeros segmentos

¿Qué sucede si quiero precargar especulativamente cierto contenido multimedia sin saber qué medio de comunicación el usuario elegirá finalmente? Si el usuario se encuentra en un página web que contiene 10 videos, probablemente, tengamos memoria suficiente segmento a cada archivo, pero no deberíamos crear 10 <video> y 10 objetos MediaSource, y comenzarás a proporcionar esos datos.

El ejemplo de dos partes que aparece a continuación muestra cómo almacenar en caché por adelantado varios segmentos de video con la potente y fácil de usar API de Cache. Ten en cuenta que algo similar también se puede lograr con IndexedDB. Aún no usamos service workers También se puede acceder a la API de Cache desde el objeto window.

Recuperar y almacenar en caché

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;
    });
  });
}

Ten en cuenta que, si usara solicitudes Range HTTP, tendría que volver a crearlas de forma manual Un objeto Response, ya que la API de Cache todavía no admite respuestas Range. Sé Ten en cuenta que llamar a networkResponse.arrayBuffer() recupera todo el contenido de la respuesta en la memoria del renderizador. o en rangos pequeños.

Como referencia, modifiqué parte del ejemplo anterior para guardar el rango HTTP a la caché previa de video.

    ...
    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;
    });

Reproducir video

Cuando un usuario haga clic en un botón de reproducción, recuperaremos el primer segmento del video. disponible en la API de Cache para que la reproducción comience de inmediato (si está disponible). De lo contrario, simplemente los obtenemos de la red. Ten en cuenta que los navegadores y los usuarios pueden decidir borrar la Caché.

Como vimos antes, usamos ECM para ingresar el primer segmento del video. .

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.
      });
    }
  });
}

Crea respuestas de Range con un service worker

¿Qué sucede si recuperaste un archivo de video completo y lo guardaste en la API de Cache? Cuando el navegador envía una solicitud Range HTTP, seguramente no todo el video en la memoria del procesador, ya que la API de Cache no Se admiten respuestas de Range todavía.

Entonces, te mostraré cómo interceptar estas solicitudes y mostrar un Range personalizado. respuesta de 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;
  }
}

Es importante destacar que usé response.blob() para recrear la porción ya que esto me permite manejar el archivo response.arrayBuffer() lleva todo el archivo a la memoria del procesador.

Se puede usar mi encabezado HTTP X-From-Cache personalizado para saber si esta solicitud provenga de la caché o de la red. La puede usar un jugador, como ShakaPlayer para ignorar el tiempo de respuesta como indicador de la velocidad de la red.

Consulta la app de Samples oficial y, en particular, su archivo ranged-response.js para obtener una solución completa para controlar Range solicitudes.