PWA mit Offlinestreaming

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Progressive Web-Apps bieten viele Funktionen, die zuvor nativen Apps vorbehalten waren. Web-Anwendungen. Eines der wichtigsten Merkmale von PWAs sind offline.

Noch besser wäre das Offline-Streaming von Medien, Erweiterung, die Sie Ihren Nutzenden auf unterschiedliche Weise anbieten können. Sie können jedoch Dies stellt ein wirklich einzigartiges Problem dar: Mediendateien können sehr groß sein. Also fragen Sie sich vielleicht:

  • Wie lade ich eine große Videodatei herunter und speichere sie?
  • Und wie stelle ich sie den Nutzenden bereit?

In diesem Artikel werden wir auf diese Fragen eingehen. Wir beziehen uns auf die von uns entwickelte Kino-Demo-PWA, die Ihnen praktische wie Sie Offline-Streaming-Medien implementieren können, mit jedem funktionalen oder Präsentationskonzept. Die folgenden Beispiele sind in erster Linie zu Bildungszwecken. In den meisten Fällen empfiehlt es sich, eines der bestehenden Media Frameworks, um diese Funktionen bereitzustellen.

Es sei denn, Sie haben einen guten Business Case, um eine eigene oder eine PWA zu entwickeln. mit Offline-Streaming. In diesem Artikel erfahren Sie, Die APIs und Techniken, die verwendet werden, um Nutzern qualitativ hochwertige Offlinemedien zur Verfügung zu stellen Nutzererfahrung.

Große Mediendatei herunterladen und speichern

Progressive Web-Apps nutzen in der Regel die praktische Cache API für Downloads und speichern Sie die für die Offline-Nutzung erforderlichen Assets: Dokumente, Stylesheets, Bilder und mehr.

Hier ist ein einfaches Beispiel für die Verwendung der Cache API in einem 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',
      ]);
    })
  );
});

Das obige Beispiel funktioniert zwar technisch, aber bei Verwendung der Cache API gibt es mehrere Einschränkungen, die die Verwendung mit großen Dateien unpraktisch machen.

Die Cache API tut z. B. Folgendes nicht:

  • Ermöglicht es, Downloads ganz einfach zu pausieren und fortzusetzen
  • Sie können den Fortschritt von Downloads verfolgen.
  • Anbieten einer Möglichkeit, ordnungsgemäß auf HTTP-Bereichsanfragen zu antworten

All diese Probleme stellen ziemlich schwerwiegende Einschränkungen für jede Video-App dar. Sehen wir uns einige andere Optionen an, die möglicherweise besser geeignet sind.

Die Fetch API ist heute eine browserübergreifende Möglichkeit für den asynchronen Zugriff auf Remote- Dateien. In unserem Anwendungsfall können Sie so auf große Videodateien als Stream zugreifen speichern Sie sie mithilfe einer HTTP-Bereichsanfrage inkrementell als Blöcke.

Nachdem Sie die Datenblöcke nun mit der Fetch API gelesen haben, müssen Sie speichern können. Wahrscheinlich sind mit Ihren Medien mehrere Metadaten verknüpft. Datei wie Name, Beschreibung, Laufzeitlänge, Kategorie usw.

Sie speichern nicht nur die eine Mediendatei, sondern ein strukturiertes Objekt. und die Mediendatei ist nur eine ihrer Eigenschaften.

In diesem Fall bietet die IndexedDB API eine hervorragende Lösung zum Speichern Mediendaten und Metadaten. Es kann einfach riesige Mengen an Binärdaten speichern bietet außerdem Indexe, mit denen Sie sehr schnelle Datensuchen durchführen können.

Mediendateien mithilfe der Fetch API herunterladen

In unserer Demo-PWA haben wir einige interessante Funktionen rund um die Fetch API entwickelt. den wir Kino genannt haben – der Quellcode ist öffentlich, also können Sie ihn sich ansehen.

  • Die Möglichkeit, unvollständige Downloads zu pausieren und fortzusetzen
  • Ein benutzerdefinierter Puffer zum Speichern von Datenblöcken in der Datenbank.

Bevor wir zeigen, wie diese Funktionen implementiert werden, kurze Zusammenfassung, wie Sie das Fetch API zum Herunterladen von Dateien verwenden können.

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

Siehst du, dass sich await reader.read() in einer Schleife befindet? So erhalten Sie Blöcke aus einem lesbaren Stream beim Empfang von Daten aus dem Netzwerk. Überlegen Sie, wie nützlich ist: Sie können mit der Verarbeitung der Daten beginnen, noch bevor sie eintreffen. aus dem Netzwerk.

Downloads fortsetzen

Wenn ein Download angehalten oder unterbrochen wird, werden die eingegangenen Datenblöcke in einer IndexedDB-Datenbank gespeichert werden kann. Sie können dann eine Schaltfläche anzeigen, einen Download in Ihrer Anwendung fortsetzen. Da der PWA-Server für die Demo von Kino unterstützt HTTP-Bereichsanfragen. Das Fortsetzen eines Downloads ist recht einfach:

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

Benutzerdefinierter Schreibpuffer für IndexedDB

Auf Papier: das Schreiben von dataChunk-Werten in eine IndexedDB-Datenbank ist einfach. Diese Werte sind bereits ArrayBuffer Instanzen, die speicherbar sind direkt in IndexedDB verwenden, sodass wir einfach ein Objekt mit einer passenden Form erstellen können. und speichern Sie sie.

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 = () => { ... }

Obwohl dieser Ansatz funktioniert, werden Sie wahrscheinlich feststellen, dass Ihre IndexedDB schreibt. deutlich langsamer sind als Ihr Download. Das liegt nicht daran, dass IndexedDB schreibt. sind langsam. Das liegt daran, dass wir durch die Erstellung für jeden Datenblock, den wir von einem Netzwerk erhalten.

Die heruntergeladenen Blöcke können ziemlich klein sein und vom Stream in aufeinanderfolgen. Sie müssen die Rate der IndexedDB-Schreibvorgänge begrenzen. Im Für die Kino-Demo-PWA wird ein intermediärer Schreibpuffer implementiert.

Wenn Datenblöcke aus dem Netzwerk eintreffen, hängen wir sie zuerst an unseren Zwischenspeicher an. Wenn die eingehenden Daten nicht passen, leeren wir den gesamten Puffer in die Datenbank löschen, bevor Sie die restlichen Daten anhängen. Die IndexedDB Schreibvorgänge sind weniger häufig, was zu erheblich verbesserten Schreibvorgängen führt. die Leistung.

Bereitstellen einer Mediendatei aus dem Offlinespeicher

Nachdem Sie eine Mediendatei heruntergeladen haben, soll Ihr Service Worker aus IndexedDB bereitstellen, anstatt sie aus dem Netzwerk abzurufen.

/**
 * 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);

Was müssen Sie also in getVideoResponse() tun?

  • Die Methode event.respondWith() erwartet ein Response-Objekt als Parameter.

  • Der Response()-Konstruktor teilt uns mit, dass es mehrere Objekttypen gibt, die wir könnte zur Instanziierung eines Response-Objekts verwendet werden: Blob, BufferSource, ReadableStream und mehr.

  • Wir brauchen ein Objekt, das nicht alle Daten im Speicher sollten Sie sich für ReadableStream entscheiden.

Da wir es mit großen Dateien zu tun haben, wollten wir Browser auch erlauben, nur den Teil der Datei anfordern, der aktuell benötigt wird, grundlegende Unterstützung für HTTP-Bereichsanfragen.

/**
 * 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;

Sie können sich den Kino-Demo-PWA für Service Worker-Quellcode ansehen, um wie wir Dateidaten aus IndexedDB auslesen und einen Stream einer echten Anwendung.

Weitere Hinweise

Nachdem Sie sich die wichtigsten Hindernisse aus dem Weg geräumt haben, können Sie jetzt einige nützlichen Funktionen für Ihre Video-App. Hier sind einige Beispiele für Funktionen, die du in der Kino-Demo-PWA findest:

  • Integration der Media Session API, mit der Nutzer Medien steuern können Wiedergabe über spezielle Hardware-Medientasten oder über Medienbenachrichtigungen Pop-ups.
  • Caching anderer Assets, die mit Mediendateien verknüpft sind, z. B. Untertitel Posterbilder mithilfe der guten alten Cache API.
  • Download von Videostreams (DASH, HLS) in der App wird unterstützt. Weil-Stream in der Regel mehrere Quellen mit unterschiedlichen Bitraten angeben, Manifestdatei umwandeln und vor dem Speichern nur eine Medienversion herunterladen um sie offline anzusehen.

Als Nächstes geht es um Schnelle Wiedergabe mit Vorabladen von Audio und Video.