AWP con transmisión sin conexión

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Las apps web progresivas aportan muchas funciones que antes se reservaban para los anuncios nativos. aplicaciones a la Web. Una de las características más importantes asociadas con Las AWP son una experiencia sin conexión.

Aún mejor sería una experiencia de transmisión de medios sin conexión, que es una que podrías ofrecer a tus usuarios de varias maneras. Sin embargo, Esto crea un problema realmente único: los archivos multimedia pueden ser muy grandes. De esta manera, quizás te preguntes:

  • ¿Cómo descargo y almaceno un archivo de video grande?
  • ¿Y cómo se la entrego al usuario?

En este artículo, analizaremos las respuestas a estas preguntas y consulta la AWP de demostración de Kino que creamos y que te proporciona herramientas ejemplos de cómo implementar una experiencia de transmisión de medios sin conexión usando cualquier framework funcional o de presentación. Los siguientes ejemplos son principalmente con fines educativos, ya que en la mayoría de los casos deberías usar uno de los marcos de trabajo de medios existentes para proporcionar estas funciones.

A menos que tengas un buen caso empresarial para desarrollar el tuyo, crear una AWP de la transmisión sin conexión tiene sus desafíos. En este artículo, obtendrás información sobre las APIs y las técnicas utilizadas para proporcionar a los usuarios medios sin conexión de alta calidad una experiencia fluida a los desarrolladores.

Cómo descargar y almacenar un archivo multimedia grande

Por lo general, las apps web progresivas usan la conveniente API de Cache para realizar descargas y almacenar los recursos necesarios para brindar una experiencia sin conexión: documentos, hojas de estilo, imágenes y otros.

Aquí tienes un ejemplo básico del uso de la API de Cache dentro de un Service Worker:

const cacheStorageName = 'v1';

this.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheStorageName).then(function(cache) {
      return cache.addAll([
        'index.html',
        'style.css',
        'scripts.js',

        // Don't do this.
        'very-large-video.mp4',
      ]);
    })
  );
});

Si bien el ejemplo anterior funciona técnicamente, usar la Cache API tiene varias limitaciones que dificultan su uso con archivos grandes.

Por ejemplo, la API de Cache no realiza las siguientes acciones:

  • Te permiten pausar y reanudar descargas con facilidad
  • Permitirte realizar un seguimiento del progreso de las descargas
  • Ofrecer una forma de responder correctamente a las solicitudes de rango HTTP

Todos estos problemas son limitaciones bastante serias para cualquier aplicación de video. Revisemos otras opciones que podrían ser más adecuadas.

Hoy en día, la API de Fetch es una manera entre navegadores de acceder a datos remotos de forma asíncrona archivos. En nuestro caso de uso, te permite acceder a archivos de video grandes como una transmisión y almacenarlos de forma incremental como fragmentos con una solicitud de rango HTTP.

Ahora que puedes leer los fragmentos de datos con la API de Fetch, también debes hacer lo siguiente: almacenarlos. Es probable que haya muchos metadatos asociados a tu contenido multimedia como nombre, descripción, duración del tiempo de ejecución, categoría, etcétera.

No almacena solo un archivo multimedia, sino un objeto estructurado y el archivo multimedia es solo una de sus propiedades.

En este caso, la API de IndexedDB brinda una solución excelente para almacenar datos y metadatos de medios. Puede contener grandes cantidades de datos binarios también ofrece índices que te permiten realizar búsquedas de datos muy rápidas.

Descarga archivos multimedia con la API de Fetch

Desarrollamos un par de funciones interesantes en torno a la API de Fetch en nuestra AWP de demostración, que llamamos Kino y el código fuente es público, así que puedes revisarlo.

  • Capacidad de pausar y reanudar descargas incompletas
  • Es un búfer personalizado para almacenar fragmentos de datos en la base de datos.

Antes de mostrar cómo se implementan esas funciones, un breve resumen de cómo puedes usar la Fetch API para descargar archivos.

/**
 * Downloads a single file.
 *
 * @param {string} url URL of the file to be downloaded.
 */
async function downloadFile(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  do {
    const { done, dataChunk } = await reader.read();
    // Store the `dataChunk` to IndexedDB.
  } while (!done);
}

¿Notas que await reader.read() está en un bucle? Así es como recibirás los bloques de datos de una transmisión legible a medida que llegan desde la red. Considera cómo Esto es útil porque se pueden empezar a procesar datos incluso antes de que lleguen todos de la red.

Reanudando descargas

Cuando se pausa o interrumpe una descarga, los bloques de datos que llegaron almacenarse de forma segura en una base de datos IndexedDB. Luego, puedes mostrar un botón para reanudar una descarga en tu aplicación. Debido a que el servidor de AWP de demostración de Kino admite solicitudes de rango HTTP para reanudar una descarga es algo sencillo:

async downloadFile() {
  // this.currentFileMeta contains data from IndexedDB.
  const { bytesDownloaded, url, downloadUrl } = this.currentFileMeta;
  const fetchOpts = {};

  // If we already have some data downloaded,
  // request everything from that position on.
  if (bytesDownloaded) {
    fetchOpts.headers = {
      Range: `bytes=${bytesDownloaded}-`,
    };
  }

  const response = await fetch(downloadUrl, fetchOpts);
  const reader = response.body.getReader();

  let dataChunk;
  do {
    dataChunk = await reader.read();
    if (!dataChunk.done) this.buffer.add(dataChunk.value);
  } while (!dataChunk.done && !this.paused);
}

Búfer de escritura personalizado para IndexedDB

En papel, el proceso de escribir valores dataChunk en una base de datos IndexedDB es sencillo. Esos valores ya son instancias de ArrayBuffer, que se pueden almacenar. directamente en IndexedDB, para que podamos crear un objeto con una forma adecuada y almacenarlos.

const dataItem = {
  url: fileUrl,
  rangeStart: dataStartByte,
  rangeEnd: dataEndByte,
  data: dataChunk,
}

// Name of the store that will hold your data.
const storeName = 'fileChunksStorage'

// `db` is an instance of `IDBDatabase`.
const transaction = db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const putRequest = store.put(data);

putRequest.onsuccess = () => { ... }

Aunque este enfoque funciona, descubrirás que la base de datos indexada escribe son significativamente más lentas que la descarga. Esto no se debe a que IndexedDB escribe son lentas, porque agregamos mucha sobrecarga transaccional una nueva transacción por cada bloque de datos que recibimos de una red.

Los fragmentos descargados pueden ser bastante pequeños y los puede emitir la transmisión en una sucesión rápida. Debes limitar la tasa de escrituras de IndexedDB. En la Kino es la AWP de demostración. Para ello, implementamos un búfer de escritura intermedio.

A medida que los bloques de datos llegan desde la red, primero los anexamos a nuestro búfer. Si los datos entrantes no caben, vaciamos el búfer completo en la base de datos y borrarlos antes de agregar el resto de los datos. Como resultado, nuestra base de datos IndexedDB Las operaciones de escritura son menos frecuentes, lo que se traduce en una mejora significativa de las operaciones de escritura. rendimiento.

Cómo entregar un archivo multimedia desde el almacenamiento sin conexión

Una vez que hayas descargado un archivo multimedia, probablemente quieras que tu service worker entregarlo desde IndexedDB en lugar de recuperar el archivo desde la red.

/**
 * The main service worker fetch handler.
 *
 * @param {FetchEvent} event Fetch event.
 */
const fetchHandler = async (event) => {
  const getResponse = async () => {
    // Omitted Cache API code used to serve static assets.

    const videoResponse = await getVideoResponse(event);
    if (videoResponse) return videoResponse;

    // Fallback to network.
    return fetch(event.request);
  };
  event.respondWith(getResponse());
};
self.addEventListener('fetch', fetchHandler);

¿Qué debes hacer en getVideoResponse()?

  • El método event.respondWith() espera un objeto Response como parámetro.

  • El constructor Response() nos indica que hay varios tipos de objetos que para crear una instancia de un objeto Response: Blob, BufferSource, ReadableStream y más.

  • Necesitamos un objeto que no tenga todos sus datos en la memoria, probablemente quieras elegir ReadableStream.

Además, como se trata de archivos grandes, y queríamos permitir que los navegadores solo solicitaba la parte del archivo que necesita actualmente, necesitábamos implementar compatibilidad básica con las solicitudes de rango HTTP.

/**
 * Respond to a request to fetch offline video file and construct a response
 * stream.
 *
 * Includes support for `Range` requests.
 *
 * @param {Request} request  Request object.
 * @param {Object}  fileMeta File meta object.
 *
 * @returns {Response} Response object.
 */
const getVideoResponse = (request, fileMeta) => {
  const rangeRequest = request.headers.get('range') || '';
  const byteRanges = rangeRequest.match(/bytes=(?<from>[0-9]+)?-(?<to>[0-9]+)?/);

  // Using the optional chaining here to access properties of
  // possibly nullish objects.
  const rangeFrom = Number(byteRanges?.groups?.from || 0);
  const rangeTo = Number(byteRanges?.groups?.to || fileMeta.bytesTotal - 1);

  // Omitting implementation for brevity.
  const streamSource = {
     pull(controller) {
       // Read file data here and call `controller.enqueue`
       // with every retrieved chunk, then `controller.close`
       // once all data is read.
     }
  }
  const stream = new ReadableStream(streamSource);

  // Make sure to set proper headers when supporting range requests.
  const responseOpts = {
    status: rangeRequest ? 206 : 200,
    statusText: rangeRequest ? 'Partial Content' : 'OK',
    headers: {
      'Accept-Ranges': 'bytes',
      'Content-Length': rangeTo - rangeFrom + 1,
    },
  };
  if (rangeRequest) {
    responseOpts.headers['Content-Range'] = `bytes ${rangeFrom}-${rangeTo}/${fileMeta.bytesTotal}`;
  }
  const response = new Response(stream, responseOpts);
  return response;

Puedes consultar el código fuente del service worker de la AWP de demostración de Kino para encontrar lo siguiente: cómo leemos datos de archivos de IndexedDB y construimos una transmisión en una aplicación real.

Otras consideraciones

Una vez que te hayas alejado de los principales obstáculos, puedes comenzar a agregar interesantes para tu aplicación de video. Estos son algunos ejemplos de funciones que encontrarás en la AWP de demostración de Kino:

  • La API de Media Session que permite a los usuarios controlar el contenido multimedia Reproducción con teclas multimedia de hardware dedicadas o desde notificaciones multimedia las ventanas emergentes.
  • El almacenamiento en caché de otros activos asociados con los archivos multimedia, como subtítulos de pósteres con la antigua API de Cache.
  • Se admite la descarga de transmisiones de video por Internet (DASH, HLS) dentro de la app. Porque la transmisión los manifiestos generalmente declaran varias fuentes de diferentes tasas de bits, debes transformar el archivo de manifiesto y descargar solo una versión multimedia antes de almacenar para verlo sin conexión.

A continuación, aprenderás sobre la Reproducción rápida con precarga de audio y video.