Data di pubblicazione: 5 luglio 2021
Le app web progressive portano sul web molte funzionalità precedentemente riservate alle applicazioni native. Una delle funzionalità più importanti associate alle PWA è l'esperienza offline.
Ancora meglio sarebbe un'esperienza di streaming multimediale offline, un miglioramento che potresti offrire ai tuoi utenti in diversi modi. Tuttavia, questo crea un problema davvero unico: i file multimediali possono essere molto grandi. Quindi potresti chiederti:
- Come faccio a scaricare e archiviare un file video di grandi dimensioni?
- E come faccio a mostrarlo all'utente?
In questo articolo risponderemo a queste domande facendo riferimento alla PWA demo Kino che abbiamo creato e che ti fornisce esempi pratici di come implementare un'esperienza di streaming multimediale offline senza utilizzare framework funzionali o di presentazione. Gli esempi seguenti sono principalmente a scopo didattico, perché nella maggior parte dei casi dovresti probabilmente utilizzare uno dei framework multimediali esistenti per fornire queste funzionalità.
A meno che tu non abbia un buon business case per sviluppare la tua, la creazione di una PWA con lo streaming offline presenta delle difficoltà. In questo articolo scoprirai le API e le tecniche utilizzate per offrire agli utenti un'esperienza multimediale offline di alta qualità.
Download e archiviazione di un file multimediale di grandi dimensioni
Le app web progressive utilizzano in genere la comoda API Cache per scaricare e archiviare gli asset necessari per fornire l'esperienza offline: documenti, fogli di stile, immagini e altri.
Ecco un esempio di base di utilizzo dell'API Cache in 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',
]);
})
);
});
Sebbene l'esempio precedente funzioni tecnicamente, l'utilizzo dell'API Cache presenta diverse limitazioni che rendono impraticabile il suo utilizzo con file di grandi dimensioni.
Ad esempio, l'API Cache non:
- Consentono di mettere in pausa e riprendere facilmente i download
- Consentono di monitorare l'avanzamento dei download
- Offrire un modo per rispondere correttamente alle richieste di intervallo HTTP
Tutti questi problemi rappresentano limiti piuttosto seri per qualsiasi applicazione video. Esaminiamo altre opzioni che potrebbero essere più appropriate.
Al giorno d'oggi, l'API Fetch è un modo cross-browser per accedere in modo asincrono a file remoti. Nel nostro caso d'uso, ti consente di accedere a file video di grandi dimensioni come stream e di memorizzarli in modo incrementale come blocchi utilizzando una richiesta di intervallo HTTP.
Ora che puoi leggere i blocchi di dati con l'API Fetch, devi anche memorizzarli. È probabile che al file multimediale siano associati diversi metadati, ad esempio nome, descrizione, durata, categoria e così via.
Non memorizzi un solo file multimediale, ma un oggetto strutturato e il file multimediale è solo una delle sue proprietà.
In questo caso, l'API IndexedDB fornisce un'ottima soluzione per archiviare sia i dati multimediali che i metadati. Può contenere facilmente enormi quantità di dati binari e offre anche indici che consentono di eseguire ricerche di dati molto rapide.
Download di file multimediali utilizzando l'API Fetch
Nella nostra PWA demo, che abbiamo chiamato Kino, abbiamo creato un paio di funzionalità interessanti basate sull'API Fetch. Il codice sorgente è pubblico, quindi puoi esaminarlo.
- Possibilità di mettere in pausa e riprendere i download incompleti.
- Un buffer personalizzato per l'archiviazione di blocchi di dati nel database.
Prima di mostrare come vengono implementate queste funzionalità, faremo un breve riepilogo di come utilizzare l'API Fetch per scaricare i file.
/**
* 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);
}
Noti che await reader.read()
è in un loop? In questo modo riceverai blocchi
di dati da un flusso leggibile man mano che arrivano dalla rete. Considera quanto
sia utile: puoi iniziare a elaborare i dati prima ancora che arrivino tutti
dalla rete.
Ripresa dei download
Quando un download viene messo in pausa o interrotto, i blocchi di dati arrivati vengono archiviati in modo sicuro in un database IndexedDB. Puoi quindi visualizzare un pulsante per riprendere un download nella tua applicazione. Poiché il server PWA demo Kino supporta le richieste di intervallo HTTP, la ripresa di un download è piuttosto semplice:
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);
}
Buffer di scrittura personalizzato per IndexedDB
Su carta, la procedura di scrittura dei valori dataChunk
in un database IndexedDB
è semplice. Questi valori sono già istanze ArrayBuffer
, memorizzabili
direttamente in IndexedDB, quindi possiamo semplicemente creare un oggetto di forma appropriata
e memorizzarlo.
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 = () => { ... }
Sebbene questo approccio funzioni, probabilmente noterai che le scritture IndexedDB sono molto più lente del download. Non perché le scritture IndexedDB siano lente, ma perché aggiungiamo molti overhead transazionali creando una nuova transazione per ogni blocco di dati che riceviamo da una rete.
I segmenti scaricati possono essere piuttosto piccoli e possono essere emessi dal flusso in rapida successione. Devi limitare la frequenza delle scritture IndexedDB. Nella PWA demo Kino, questa operazione viene eseguita implementando un buffer di scrittura intermedio.
Man mano che i blocchi di dati arrivano dalla rete, li aggiungiamo prima al nostro buffer. Se i dati in entrata non rientrano, svuotiamo l'intero buffer nel database e lo cancelliamo prima di aggiungere il resto dei dati. Di conseguenza, le scritture IndexedDB sono meno frequenti, il che comporta un miglioramento significativo delle prestazioni di scrittura.
Pubblicazione di un file multimediale dall'archivio offline
Una volta scaricato un file multimediale, probabilmente vuoi che il service worker lo fornisca da IndexedDB anziché recuperarlo dalla rete.
/**
* 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);
Quindi, cosa devi fare in getVideoResponse()
?
Il metodo
event.respondWith()
prevede un oggettoResponse
come parametro.Il costruttore Response() ci dice che esistono diversi tipi di oggetti che potremmo utilizzare per creare un'istanza di un oggetto
Response
: unBlob
,BufferSource
,ReadableStream
e altro ancora.Abbiamo bisogno di un oggetto che non contenga tutti i suoi dati in memoria, quindi probabilmente sceglieremo
ReadableStream
.
Inoltre, poiché abbiamo a che fare con file di grandi dimensioni e volevamo consentire ai browser di richiedere solo la parte del file di cui hanno bisogno al momento, abbiamo dovuto implementare un supporto di base per le richieste di intervallo 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;
Non esitare a consultare il Kino per scoprire come leggiamo i dati dei file da IndexedDB e costruiamo un flusso in un'applicazione reale.
Altre considerazioni
Ora che hai superato gli ostacoli principali, puoi iniziare ad aggiungere alcune funzionalità utili alla tua applicazione video. Ecco alcuni esempi di funzionalità che troverai nella PWA demo Kino:
- Integrazione dell'API Media Session che consente agli utenti di controllare la riproduzione dei contenuti multimediali utilizzando tasti multimediali hardware dedicati o dai popup di notifica multimediale.
- Memorizzazione nella cache di altri asset associati ai file multimediali, come sottotitoli codificati e immagini di copertina, utilizzando la vecchia API Cache.
- Supporto per il download di stream video (DASH, HLS) all'interno dell'app. Poiché i manifest degli stream in genere dichiarano più origini di bitrate diversi, devi trasformare il file manifest e scaricare una sola versione multimediale prima di archiviarla per la visualizzazione offline.
A seguire, scoprirai di più sulla riproduzione rapida con precaricamento di audio e video.