Opublikowano: 5 lipca 2021 r.
Progresywne aplikacje internetowe udostępniają w internecie wiele funkcji, które wcześniej były dostępne tylko w aplikacjach natywnych. Jedną z najważniejszych funkcji związanych z PWA jest możliwość korzystania z nich w trybie offline.
Jeszcze lepsze byłoby strumieniowe przesyłanie multimediów w trybie offline, które możesz zaoferować użytkownikom na kilka sposobów. Powoduje to jednak wyjątkowy problem – pliki multimedialne mogą być bardzo duże. Możesz więc zapytać:
- Jak pobrać i zapisać duży plik wideo?
- Jak wyświetlić go użytkownikowi?
W tym artykule znajdziesz odpowiedzi na te pytania. Będziemy się przy tym odwoływać do demonstracyjnej progresywnej aplikacji internetowej Kino, którą stworzyliśmy, aby pokazać Ci praktyczne przykłady wdrożenia strumieniowego przesyłania multimediów w trybie offline bez użycia żadnych frameworków funkcjonalnych ani prezentacyjnych. Poniższe przykłady mają głównie charakter edukacyjny, ponieważ w większości przypadków do udostępniania tych funkcji należy używać jednego z istniejących frameworków multimedialnych.
Jeśli nie masz dobrego uzasadnienia biznesowego dla opracowania własnej aplikacji PWA, jej utworzenie z funkcją strumieniowania offline może być trudne. Z tego artykułu dowiesz się więcej o interfejsach API i technikach, które umożliwiają zapewnienie użytkownikom wysokiej jakości multimediów offline.
pobieranie i przechowywanie dużego pliku multimedialnego,
Progresywne aplikacje internetowe zwykle korzystają z wygodnego interfejsu Cache API, aby pobierać i przechowywać zasoby wymagane do działania w trybie offline: dokumenty, arkusze stylów, obrazy i inne.
Oto podstawowy przykład użycia interfejsu Cache API w usłudze 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',
      ]);
    })
  );
});
Powyższy przykład działa, ale korzystanie z interfejsu Cache API ma kilka ograniczeń, które sprawiają, że używanie go w przypadku dużych plików jest niepraktyczne.
Na przykład interfejs Cache API nie:
- umożliwia łatwe wstrzymywanie i wznawianie pobierania;
- umożliwia śledzenie postępu pobierania;
- zapewniać możliwość prawidłowego odpowiadania na żądania zakresu HTTP;
Wszystkie te problemy stanowią poważne ograniczenia dla każdej aplikacji wideo. Przyjrzyjmy się innym opcjom, które mogą być bardziej odpowiednie.
Obecnie Fetch API to sposób na asynchroniczny dostęp do zdalnych plików w różnych przeglądarkach. W naszym przypadku umożliwia to dostęp do dużych plików wideo w formie strumienia i przechowywanie ich przyrostowo w postaci fragmentów za pomocą żądania zakresu HTTP.
Teraz, gdy możesz odczytywać fragmenty danych za pomocą interfejsu Fetch API, musisz je też przechowywać. Z plikiem multimedialnym jest prawdopodobnie powiązanych wiele metadanych, takich jak nazwa, opis, czas trwania, kategoria itp.
Nie przechowujesz tylko jednego pliku multimedialnego, ale obiekt strukturalny, a plik multimedialny jest tylko jedną z jego właściwości.
W takim przypadku IndexedDB API jest doskonałym rozwiązaniem do przechowywania zarówno danych multimedialnych, jak i metadanych. Może ona łatwo przechowywać ogromne ilości danych binarnych, a także oferuje indeksy, które umożliwiają bardzo szybkie wyszukiwanie danych.
Pobieranie plików multimedialnych za pomocą interfejsu Fetch API
W naszej demonstracyjnej aplikacji PWA o nazwie Kino stworzyliśmy kilka ciekawych funkcji opartych na interfejsie Fetch API. Kod źródłowy jest publiczny, więc możesz go przejrzeć.
- Możliwość wstrzymywania i wznawiania niekompletnych pobrań.
- Niestandardowy bufor do przechowywania w bazie danych fragmentów danych.
Zanim pokażemy, jak te funkcje są wdrażane, najpierw przypomnimy, jak można używać interfejsu Fetch API do pobierania plików.
/**
 * 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);
}
Zwróć uwagę, że await reader.read() jest w pętli. W ten sposób będziesz otrzymywać fragmenty danych ze strumienia do odczytu w miarę ich przesyłania przez sieć. Zastanów się, jak przydatne jest to rozwiązanie: możesz rozpocząć przetwarzanie danych, zanim wszystkie zostaną przesłane z sieci.
Wznawianie pobierania
Gdy pobieranie zostanie wstrzymane lub przerwane, przychodzące fragmenty danych będą bezpiecznie przechowywane w bazie danych IndexedDB. Następnie możesz wyświetlić w aplikacji przycisk umożliwiający wznowienie pobierania. Ponieważ serwer demonstracyjnej progresywnej aplikacji internetowej Kino obsługuje żądania zakresu HTTP, 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
Teoretycznie proces zapisywania wartości dataChunk w bazie danych IndexedDB jest prosty. Te wartości są już instancjami ArrayBuffer, które można bezpośrednio przechowywać w IndexedDB, więc wystarczy utworzyć obiekt o odpowiednim kształcie i go zapisać.
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 zauważysz, że zapisywanie w IndexedDB jest znacznie wolniejsze niż pobieranie. Nie wynika to z powolnego zapisu w IndexedDB, ale z dużego obciążenia transakcyjnego spowodowanego tworzeniem nowej transakcji dla każdego fragmentu danych otrzymywanego z sieci.
Pobrane fragmenty mogą być dość małe i mogą być emitowane przez strumień w szybkim tempie. Musisz ograniczyć szybkość zapisów w IndexedDB. W demonstracyjnej progresywnej aplikacji internetowej Kino robimy to, implementując pośredni bufor zapisu.
Gdy z sieci docierają fragmenty danych, najpierw dołączamy je do bufora. Jeśli przychodzące dane się nie mieszczą, opróżniamy cały bufor do bazy danych i czyścimy go przed dołączeniem pozostałych danych. Dzięki temu zapisy w IndexedDB są rzadsze, co znacznie poprawia wydajność zapisu.
Wyświetlanie pliku multimedialnego z pamięci offline
Po pobraniu pliku multimedialnego prawdopodobnie chcesz, aby service worker serwował go z IndexedDB zamiast pobierać 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 trzeba zrobić w getVideoResponse()?
- Metoda - event.respondWith()oczekuje obiektu- Responsejako parametru.
- Z konstruktora Response() dowiadujemy się, że do utworzenia obiektu - Responsemożna użyć kilku typów obiektów:- Blob,- BufferSource,- ReadableStreami innych.
- Potrzebujemy obiektu, który nie przechowuje wszystkich danych w pamięci, więc prawdopodobnie wybierzemy - ReadableStream.
Poza tym, ponieważ mamy do czynienia z dużymi plikami i chcieliśmy umożliwić przeglądarkom żądanie tylko tej części pliku, której aktualnie potrzebują, musieliśmy wdrożyć podstawową obsługę żądań zakresu 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;
Zapoznaj się z kodem źródłowym usługi Kino, która jest demonstracyjną progresywną aplikacją internetową, aby dowiedzieć się, jak odczytujemy dane z plików w IndexedDB i tworzymy strumień w prawdziwej aplikacji.
Inne uwagi
Gdy pokonasz główne przeszkody, możesz zacząć dodawać do aplikacji wideo przydatne funkcje. Oto kilka przykładów funkcji, które znajdziesz w wersji demonstracyjnej Kino:
- Integracja interfejsu Media Session API, która umożliwia użytkownikom sterowanie odtwarzaniem multimediów za pomocą specjalnych klawiszy sprzętowych lub wyskakujących powiadomień o multimediach.
- buforowanie innych zasobów powiązanych z plikami multimedialnymi, takich jak napisy i obrazy plakatu, za pomocą sprawdzonego interfejsu Cache API;
- Obsługa pobierania strumieni wideo (DASH, HLS) w aplikacji. Pliki manifestu strumienia zwykle deklarują wiele źródeł o różnych szybkościach transmisji bitów, więc przed zapisaniem ich do oglądania offline musisz przekształcić plik manifestu i pobrać tylko jedną wersję multimediów.
Następnie dowiesz się więcej o szybkim odtwarzaniu z wstępnym wczytywaniem dźwięku i obrazu.
 
 
        
        