Progresywne aplikacje internetowe oferują wiele funkcji wcześniej zarezerwowanych dla natywnych aplikacji internetowych. Jedną z najważniejszych funkcji aplikacji PWAs jest możliwość korzystania z nich offline.
Jeszcze lepszym rozwiązaniem byłoby przesyłanie strumieniowe offline, które można zaoferować użytkownikom na kilka sposobów. Stwarza to jednak bardzo specyficzny problem – pliki multimedialne mogą być bardzo duże. Możesz się zastanawiać:
- Jak pobrać i przechowywać duży plik wideo?
- I jak mogę go wyświetlić użytkownikowi?
W tym artykule omówimy odpowiedzi na te pytania, odwołując się do naszej demonstracyjnej aplikacji PWA Kino, która zawiera praktyczne przykłady implementowania strumieniowego przesyłania multimediów offline bez użycia żadnych ram funkcjonalnych ani ram prezentacji. Poniższe przykłady służą głównie celom edukacyjnym, ponieważ w większości przypadków do udostępniania tych funkcji należy użyć jednej z dostępnych ramek mediów.
Jeśli nie masz dobrych argumentów biznesowych przemawiających za tworzeniem własnej aplikacji, tworzenie aplikacji internetowej z funkcją strumieniowego przesyłania offline może być trudne. Z tego artykułu dowiesz się, jakie interfejsy API i techniki są używane do zapewnienia 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 korzystania z aplikacji w trybie offline: dokumenty, skrypty, obrazy itp.
Oto podstawowy przykład użycia interfejsu Cache API w ramach usługi 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',
]);
})
);
});
Chociaż przykład powyżej technicznie działa, korzystanie z interfejsu Cache API ma pewne ograniczenia, które sprawiają, że nie jest ono praktyczne w przypadku dużych plików.
Na przykład interfejs Cache API:
- umożliwiają łatwe wstrzymywanie i wznawianie pobierania;
- umożliwiają śledzenie postępu pobierania;
- Udostępnianie sposobu prawidłowego reagowania na żądania zakresu HTTP
Wszystkie te problemy stanowią poważne ograniczenia dla każdej aplikacji do obsługi wideo. Przyjrzyjmy się innym opcjom, które mogą być bardziej odpowiednie.
Obecnie interfejs Fetch API umożliwia asynchroniczny dostęp do plików zdalnych w różnych przeglądarkach. W naszym przypadku umożliwia on dostęp do dużych plików wideo jako strumienia i ich stopniowe przechowywanie w kawałkach za pomocą żądania zakresu HTTP.
Teraz, gdy możesz odczytywać fragmenty danych za pomocą interfejsu Fetch API, musisz je też przechowywać. Z pliku multimedialnego prawdopodobnie powiązane są liczne metadane, takie jak nazwa, opis, czas trwania, kategoria itp.
Nie przechowujesz tylko jednego pliku multimedialnego, ale uporządkowany obiekt, w którym plik multimedialny jest tylko jedną z właściwości.
W takim przypadku interfejs IndexedDB API stanowi doskonałe rozwiązanie do przechowywania zarówno danych multimedialnych, jak i metadanych. Może ona z łatwością 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 wersji demonstracyjnej PWA, którą nazwaliśmy Kino, umieściliśmy kilka ciekawych funkcji związanych z interfejsem Fetch API. Kod źródłowy jest publiczny, więc możesz go przejrzeć.
- możliwość wstrzymywania i wznawiania nieukończonych pobierania;
- Niestandardowy bufor do przechowywania fragmentów danych w bazie danych.
Zanim pokażemy, jak te funkcje są wdrażane, krótko omówimy, 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);
}
Czy widzisz, że await reader.read()
jest w pętli? W ten sposób będziesz otrzymywać fragmenty danych z czytelnego strumienia, gdy będą one docierać z sieci. Pomyśl, jak to jest przydatne: możesz rozpocząć przetwarzanie danych jeszcze przed ich otrzymaniem z sieci.
Wznawianie pobierania
Gdy pobieranie zostanie wstrzymane lub przerwane, otrzymane fragmenty danych zostaną bezpiecznie zapisane w bazie danych IndexedDB. Następnie możesz wyświetlić w aplikacji przycisk wznowienia pobierania. Ponieważ serwer PWA w demo 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
Na papierze proces zapisywania wartości dataChunk
do bazy danych IndexedDB jest prosty. Te wartości są już instancjami ArrayBuffer
, które można przechowywać bezpośrednio w IndexedDB, więc możemy po prostu utworzyć obiekt o odpowiedniej formie 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 danych w IndexedDB jest znacznie wolniejsze niż pobieranie. Nie jest to spowodowane tym, że zapisywanie w IndexedDB jest powolne, ale tym, że tworzymy wiele obciążeń transakcyjnych, tworząc nową transakcję dla każdego fragmentu danych, który otrzymujemy z sieci.
Pobrane fragmenty mogą być raczej małe i mogą być emitowane przez strumień w szybkiej kolejności. Musisz ograniczyć szybkość zapisywania w IndexedDB. W demo PWA aplikacji Kino robimy to, wdrażając bufor zapisu pośredniczący.
Gdy fragmenty danych docierają z sieci, najpierw dodajemy je do naszego bufora. Jeśli napływające dane nie mieszczą się w buforze, opróżniamy go i zapisujemy w bazie danych, a potem oczyszczamy przed dodaniem reszty danych. W rezultacie operacje zapisu w IndexedDB są rzadsze, co prowadzi do znacznej poprawy wydajności zapisu.
Udostępnianie pliku multimedialnego z pamięci offline
Po pobraniu pliku multimedialnego prawdopodobnie zechcesz, aby usługa workera pobierała go z IndexedDB, a nie 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 musisz zrobić w getVideoResponse()
?
Metoda
event.respondWith()
oczekuje jako parametru obiektuResponse
.Konstruktor Response() wskazuje, że do utworzenia obiektu
Response
można użyć obiektuBlob
,BufferSource
,ReadableStream
i innych.Potrzebujemy obiektu, który nie przechowuje wszystkich danych w pamięci, więc prawdopodobnie wybierzemy
ReadableStream
.
Ponieważ mamy do czynienia z dużymi plikami i chcieliśmy umożliwić przeglądarkom żądanie tylko 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 demo Kino, czyli źródeł kodu serwisu workera w PWA, aby dowiedzieć się, jak odczytujemy dane o pliku z IndexedDB i jak tworzymy strumień w rzeczywistej aplikacji.
Inne uwagi
Po usunięciu głównych przeszkód możesz zacząć dodawać do aplikacji do tworzenia filmów przydatne funkcje. Oto kilka przykładów funkcji, które znajdziesz w demo PWA aplikacji Kino:
- Integracja z interfejsem Media Session API, która umożliwia użytkownikom sterowanie odtwarzaniem multimediów za pomocą specjalnych klawiszy sprzętowych lub wyskakujących powiadomień.
- Zapisywanie w pamięci podręcznej innych zasobów powiązanych z plikami multimedialnymi, takich jak napisy i zdjęcia plakatowe, za pomocą starego interfejsu Cache API.
- Obsługa pobierania strumieni wideo (DASH, HLS) w aplikacji. Ponieważ pliki manifestu strumienia zazwyczaj deklarują wiele źródeł o różnych szybkościach transmisji bitów, przed zapisaniem go do oglądania offline musisz przekształcić plik manifestu i pobrać tylko jedną wersję multimediów.
W następnym kroku dowiesz się więcej o szybkim odtwarzaniu z wstępnym wczytaniem dźwięku i obrazu.