PWA z obsługą strumieniowania offline

Derek Herman
Derek Herman
Jaroslav Polakovič
Jaroslav Polakovič

Progresywne aplikacje internetowe oferują wiele funkcji, które wcześniej były zarezerwowane dla reklam natywnych i aplikacjami w sieci. Jedna z najbardziej widocznych funkcji związanych z Progresywne aplikacje internetowe działają offline.

Jeszcze lepiej byłoby przesyłać strumieniowo multimedia w trybie offline, ponieważ ulepszeń, które możesz zaoferować użytkownikom na kilka różnych sposobów. Pamiętaj jednak: tworzy to naprawdę wyjątkowy problem – pliki multimedialne mogą być bardzo duże. No więc możesz zapytać:

  • Jak pobrać i przechowywać duży plik wideo?
  • Jak pokazać go użytkownikowi?

W tym artykule omówimy odpowiedzi na te pytania, przedstawiającą wersję demonstracyjną PWA Kino, którą stworzyliśmy jak można wdrożyć strumieniowane multimedia offline bez potrzeby przy użyciu struktury funkcjonalnej lub prezentacyjnej. Poniższe przykłady są głównie do celów edukacyjnych, ponieważ w większości przypadków należy jedną z istniejących platform medialnych, które umożliwiają korzystanie z tych funkcji.

Jeśli nie masz dobrego uzasadnienia do stworzenia własnej aplikacji PWA, strumieniowanie offline niesie ze sobą pewne wyzwania. Z tego artykułu dowiesz się: interfejsy API i techniki służące do zapewnienia użytkownikom wysokiej jakości mediów offline i uzyskiwanie dodatkowych informacji.

Pobieranie i przechowywanie dużego pliku multimedialnego

Progresywne aplikacje internetowe zwykle używają wygodnego interfejsu Cache API do pobierania plików oraz przechowywać zasoby niezbędne do korzystania z trybu offline: dokumenty, arkusze stylów, obrazy i inne.

Oto podstawowy przykład użycia interfejsu Cache API w skrypcie 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',
      ]);
    })
  );
});

Choć ten przykład działa technicznie, używanie interfejsu Cache API ma kilka zalet: i ograniczenia jej używania z dużymi plikami.

Na przykład interfejs Cache API nie:

  • pozwalają na łatwe wstrzymywanie i wznawianie pobierania.
  • Pozwalają śledzić postęp pobierania
  • Udostępnij sposób prawidłowego odpowiadania na żądania zakresów HTTP.

Wszystkie wymienione wyżej problemy stanowią dość poważne ograniczenia dla każdej aplikacji wideo. Przyjrzyjmy się innym opcjom, które mogą być bardziej odpowiednie.

Obecnie interfejs Fetch API pozwala asynchronicznie uzyskiwać dostęp do zdalnych danych w różnych przeglądarkach . W naszym przypadku pozwala on korzystać z dużych plików wideo przechowywać je przyrostowo jako fragmenty przy użyciu żądania dotyczącego zakresu HTTP.

Już możesz odczytywać fragmenty danych za pomocą interfejsu Fetch API. Musisz też je przechowywać. Istnieje wiele metadanych powiązanych z multimediami takie jak nazwa, opis, długość działania, kategoria itp.

Nie przechowujesz jednego pliku multimedialnego, przechowujesz obiekt uporządkowany, a plik multimedialny jest tylko jedną z jego właściwości.

W tym przypadku IndexedDB API to doskonałe rozwiązanie do przechowywania danych multimedialnych i metadanych. Potrafi łatwo przechowywać ogromne ilości danych binarnych. udostępnia też indeksy umożliwiające bardzo szybkie wyszukiwanie danych.

Pobieranie plików multimedialnych przy użyciu interfejsu Fetch API

Opracowaliśmy kilka interesujących funkcji związanych z interfejsem Fetch API w wersji demonstracyjnej PWA: którą nazwaliśmy Kinokod źródłowy jest publiczny, więc zachęcamy do zapoznania się z nim.

  • możliwość wstrzymywania i wznawiania niedokończonych pobierania plików.
  • Niestandardowy bufor do przechowywania fragmentów danych w bazie danych.

Przed pokazaniem sposobu wdrożenia tych funkcji najpierw przeprowadzimy krótkie podsumowanie pobierania plików za pomocą interfejsu Fetch API.

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

Zauważasz, że aplikacja await reader.read() jest w pętli? Tak będziesz otrzymywać fragmenty z czytelnego strumienia danych pobieranych z sieci. Zastanów się, jak co to jest przydatne: możesz zacząć przetwarzać dane jeszcze przed ich otrzymaniem z sieci.

Wznawiam pobieranie

Po wstrzymaniu lub przerwaniu pobierania, wyświetlone fragmenty danych zostaną mogą być bezpiecznie przechowywane w bazie danych IndexedDB. Następnie możesz wyświetlić przycisk, który aby wznowić pobieranie w aplikacji. Ponieważ demonstracyjny serwer PWA Kino obsługuje żądania zakresów HTTP, ale wznowienie pobierania jest dość proste:

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

Niestandardowy bufor zapisu dla IndexedDB

Na papierze proces zapisywania wartości dataChunk w bazie danych IndexedDB jest proste. Te wartości to już ArrayBuffer instancja, którą można przechowywać bezpośrednio w IndexedDB, możemy więc utworzyć obiekt o odpowiednim kształcie i przechowywać ją.

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

Chociaż to podejście działa, prawdopodobnie okaże się, że baza IndexedDB zapisuje działają znacznie wolniej niż pobieranie. Nie jest to spowodowane tym, że IndexedDB zapisuje są powolne. Powodem jest to, że generujemy duży nakład pracy transakcyjny nowa transakcja dla każdego fragmentu danych otrzymanego z sieci.

Pobrane fragmenty mogą być dość małe i mogą być emitowane przez strumień w szybkiej kolejności. Musisz ograniczyć częstotliwość zapisów w IndexedDB. W Wersja demonstracyjna Kino (PWA) w tym przypadku polega na wdrożeniu pośredniego bufora zapisu.

Wycinki danych z sieci zostają umieszczone w naszym buforze w pierwszej kolejności. Jeśli dane przychodzące nie mieszczą się, więc opróżniamy pełny bufor do bazy danych, usuń go przed dołączeniem pozostałych danych. W rezultacie nasza baza danych IndexedDB zapisy są rzadsze, co prowadzi do znacznie lepszego zapisu skuteczność reklam.

Udostępnianie pliku multimedialnego z pamięci offline

Po pobraniu pliku multimedialnego prawdopodobnie chcesz, aby skrypt service worker i udostępniać go z bazy danych IndexedDB, zamiast pobierać go z sieci.

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

Co więc musisz zrobić w aplikacji getVideoResponse()?

  • Metoda event.respondWith() oczekuje obiektu Response jako parametru.

  • Konstruktor Response() informuje, że istnieje kilka typów obiektów, można użyć do utworzenia instancji obiektu Response: Blob, BufferSource, ReadableStream i inne.

  • Potrzebujemy obiektu, który nie przechowuje wszystkich swoich danych w pamięci, prawdopodobnie wybierze ReadableStream.

Ponieważ mamy do czynienia z dużymi plikami i chcieliśmy umożliwić przeglądarkom prosić o dostęp tylko do części pliku, której potrzebują, podstawową obsługę żądań zakresów 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;

Aby dowiedzieć się więcej, zapoznaj się z kodem źródłowym skryptu service worker Kino jak odczytywać dane plików z IndexedDB i tworzyć strumień i aplikacjami.

Inne uwagi

Pozbędziesz się już innych przeszkód, więc możesz zacząć dodawać które warto mieć w swojej aplikacji wideo. Oto kilka przykładów funkcje dostępne w demonstralnej aplikacji PWA Kino:

  • Integracja interfejsu Media Session API, która umożliwia użytkownikom sterowanie multimediami odtwarzanie przy użyciu dedykowanych sprzętowych klawiszy multimedialnych lub z powiadomień o multimediach wyskakujące okienka.
  • buforowanie innych zasobów powiązanych z plikami multimedialnymi, takich jak napisy; obrazów plakatów w starym interfejsie Cache API.
  • obsługa pobierania strumieni wideo (DASH, HLS) w aplikacji; Ponieważ strumień Pliki manifestu zwykle deklarują wiele źródeł o różnych szybkościach transmisji bitów. Musisz przekształcić plik manifestu i pobrać przed zapisaniem tylko jedną wersję multimediów do przeglądania offline.

W następnej kolejności omówimy szybkie odtwarzanie z wstępnym wczytywaniem dźwięku i obrazu.