Come accelerare la riproduzione dei contenuti multimediali precaricando attivamente le risorse.
Se avvii una riproduzione più rapida, avrai più persone a guardare il tuo video o ad ascoltare il tuo audio. È un fatto noto. In questo articolo esamino le tecniche che puoi utilizzare per accelerare la riproduzione audio e video precaricando attivamente le risorse in base al tuo caso d'uso.
Descriverò tre metodi di precaricamento dei file multimediali, iniziando con i relativi pro e contro.
È magnifico... | Ma… | |
---|---|---|
Attributo di precaricamento dei video | È facile da usare per un file univoco ospitato su un server web. | I browser potrebbero ignorare completamente l'attributo. |
Il recupero delle risorse inizia quando il documento HTML è stato completamente caricato e analizzato. | ||
Le estensioni di origine media (MSE) ignorano l'attributo preload negli elementi multimediali perché è compito dell'app fornire i contenuti multimediali a MSE.
|
||
Precaricamento link |
Forza il browser a effettuare una richiesta per una risorsa video senza bloccare
l'evento onload del documento.
|
Le richieste HTTP Range non sono compatibili. |
Compatibile con MSE e segmenti di file. | Da utilizzare solo per file multimediali di piccole dimensioni (< 5 MB) quando recuperi le risorse complete. | |
Buffering manuale | Controllo completo | La gestione degli errori complessi è responsabilità del sito web. |
Attributo di precaricamento video
Se l'origine video è un file univoco ospitato su un server web, puoi utilizzare l'attributo video preload
per fornire al browser un suggerimento su quanti informazioni o contenuti precaricare. Ciò significa che Media Source Extensions (MSE) non è compatibile con preload
.
Il recupero delle risorse inizierà solo quando il documento HTML iniziale sarà stato caricato e analizzato completamente (ad es. quando è stato attivato l'evento DOMContentLoaded
), mentre l'evento load
molto diverso verrà attivato quando la risorsa sarà stata effettivamente recuperata.
L'impostazione dell'attributo preload
su metadata
indica che non è previsto che l'utente abbia bisogno del video, ma che è auspicabile recuperare i relativi metadati (dimensioni, elenco tracce, durata e così via). Tieni presente che, a partire da Chrome
64, il valore predefinito per preload
è metadata
. (in precedenza era auto
).
<video id="video" preload="metadata" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
Se l'attributo preload
viene impostato su auto
, il browser potrebbe memorizzare nella cache
un numero di dati sufficiente per poter completare la riproduzione senza richiedere un'interruzione
per un ulteriore buffering.
<video id="video" preload="auto" src="file.mp4" controls></video>
<script>
video.addEventListener('loadedmetadata', function() {
if (video.buffered.length === 0) return;
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
});
</script>
Tuttavia, ci sono alcune avvertenze. Poiché questo è solo un suggerimento, il browser potrebbe
ignorare completamente l'attributo preload
. Al momento della stesura di questo articolo, ecco alcune regole applicate in Chrome:
- Quando l'opzione Risparmio dati è attiva, Chrome forza il valore
preload
sunone
. - In Android 4.3, Chrome forza il valore
preload
anone
a causa di un bug Android. - Su una connessione di rete mobile (2G, 3G e 4G), Chrome forza il valore
preload
sumetadata
.
Suggerimenti
Se il tuo sito web contiene molte risorse video nello stesso dominio, ti consigliamo di impostare il valore preload
su metadata
o di definire l'attributo poster
e impostare preload
su none
. In questo modo eviteresti di raggiungere il numero massimo di connessioni HTTP allo stesso dominio (6, secondo la specifica HTTP 1.1) che possono bloccare il caricamento delle risorse. Tieni presente che questa operazione potrebbe anche migliorare la velocità della pagina se i video non fanno parte dell'esperienza utente principale.
Precaricamento dei link
Come trattato in altri articoli, il precaricamento dei link è un recupero dichiarativo che
consente di forzare il browser a effettuare una richiesta per una risorsa senza
bloccare l'evento load
e durante il download della pagina. Le risorse caricate tramite <link rel="preload">
vengono archiviate localmente nel browser e sono effettivamente inerti finché non viene fatto riferimento esplicito nel DOM, JavaScript o CSS.
Il precaricamento è diverso dal precaricamento perché si concentra sulla navigazione corrente e recupera le risorse con priorità in base al tipo (script, stile, carattere, video, audio e così via). Deve essere utilizzata per attivare la cache del browser per le sessioni correnti.
Precarica il video completo
Ecco come precaricare un video completo sul tuo sito web in modo che, quando il codice JavaScript chiede di recuperare i contenuti del video, questi vengano letti dalla cache perché la risorsa potrebbe essere già stata memorizzata nella cache dal browser. Se la richiesta di precaricamento non è ancora stata completata, verrà eseguito un recupero regolare dalla rete.
<link rel="preload" as="video" href="https://cdn.com/small-file.mp4">
<video id="video" controls></video>
<script>
// Later on, after some condition has been met, set video source to the
// preloaded video URL.
video.src = 'https://cdn.com/small-file.mp4';
video.play().then(() => {
// If preloaded video URL was already cached, playback started immediately.
});
</script>
Poiché la risorsa precaricata verrà utilizzata da un elemento video
nell'esempio, il valore del link di precaricamento as
è video
. Se fosse un elemento audio, sarebbe as="audio"
.
Precarica il primo segmento
L'esempio seguente mostra come precaricare il primo segmento di un video con <link
rel="preload">
e utilizzarlo con Media Source Extensions. Se non hai dimestichezza con l'API JavaScript MSE, consulta le nozioni di base su MSE.
Per semplicità, supponiamo che l'intero video sia stato suddiviso in
file più piccoli, ad esempio file_1.webm
, file_2.webm
, file_3.webm
e così via.
<link rel="preload" as="fetch" href="https://cdn.com/file_1.webm">
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// If video is preloaded already, fetch will return immediately a response
// from the browser cache (memory cache). Otherwise, it will perform a
// regular network fetch.
fetch('https://cdn.com/file_1.webm')
.then(response => response.arrayBuffer())
.then(data => {
// Append the data into the new sourceBuffer.
sourceBuffer.appendBuffer(data);
// TODO: Fetch file_2.webm when user starts playing video.
})
.catch(error => {
// TODO: Show "Video is not available" message to user.
});
}
</script>
Assistenza
Puoi rilevare il supporto di vari tipi di as
per <link rel=preload>
con gli snippet riportati di seguito:
function preloadFullVideoSupported() {
const link = document.createElement('link');
link.as = 'video';
return (link.as === 'video');
}
function preloadFirstSegmentSupported() {
const link = document.createElement('link');
link.as = 'fetch';
return (link.as === 'fetch');
}
Buffering manuale
Prima di approfondire l'API Cache e i service worker, vediamo
come eseguire il buffering manuale di un video con MSE. L'esempio riportato di seguito presuppone che il tuo server web supporti le richieste Range
HTTP, ma sarebbe abbastanza simile con i segmenti di file. Tieni presente che alcune librerie middleware, come Shaka Player di Google, JW Player e Video.js, sono progettate per gestire questo aspetto al posto tuo.
<video id="video" controls></video>
<script>
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
// Fetch beginning of the video by setting the Range HTTP request header.
fetch('file.webm', { headers: { range: 'bytes=0-567139' } })
.then(response => response.arrayBuffer())
.then(data => {
sourceBuffer.appendBuffer(data);
sourceBuffer.addEventListener('updateend', updateEnd, { once: true });
});
}
function updateEnd() {
// Video is now ready to play!
const bufferedSeconds = video.buffered.end(0) - video.buffered.start(0);
console.log(`${bufferedSeconds} seconds of video are ready to play.`);
// Fetch the next segment of video when user starts playing the video.
video.addEventListener('playing', fetchNextSegment, { once: true });
}
function fetchNextSegment() {
fetch('file.webm', { headers: { range: 'bytes=567140-1196488' } })
.then(response => response.arrayBuffer())
.then(data => {
const sourceBuffer = mediaSource.sourceBuffers[0];
sourceBuffer.appendBuffer(data);
// TODO: Fetch further segment and append it.
});
}
</script>
Considerazioni
Dato che ora hai il controllo dell'intera esperienza di buffering dei contenuti multimediali, quando pensi al precaricamento ti suggerisco di considerare il livello della batteria del dispositivo, la preferenza dell'utente "Modalità risparmio dati" e le informazioni di rete.
Consapevolezza della batteria
Considera il livello della batteria dei dispositivi degli utenti prima di precaricare un video. In questo modo, la durata della batteria viene preservata quando il livello di carica è basso.
Disattiva il precaricamento o, almeno, precarica un video a risoluzione inferiore quando la batteria del dispositivo sta per scaricarsi.
if ('getBattery' in navigator) {
navigator.getBattery()
.then(battery => {
// If battery is charging or battery level is high enough
if (battery.charging || battery.level > 0.15) {
// TODO: Preload the first segment of a video.
}
});
}
Rileva "Risparmio dati"
Utilizza l'intestazione della richiesta di suggerimento client Save-Data
per fornire applicazioni rapide e leggere agli utenti che hanno attivato la modalità "risparmio dati" nel browser. Identificando questa intestazione della richiesta, la tua applicazione può personalizzare e offrire un'esperienza utente ottimizzata agli utenti con limitazioni in termini di costi e prestazioni.
Per saperne di più, consulta la sezione Distribuzione rapida e leggera di applicazioni con risparmio di dati.
Caricamento intelligente basato sulle informazioni di rete
Ti consigliamo di controllare navigator.connection.type
prima del precaricamento. Se
viene impostato su cellular
, puoi impedire il precaricamento e informare gli utenti che
il loro operatore di rete mobile potrebbe addebitare la larghezza di banda e avviare
solo la riproduzione automatica dei contenuti memorizzati in precedenza nella cache.
if ('connection' in navigator) {
if (navigator.connection.type == 'cellular') {
// TODO: Prompt user before preloading video
} else {
// TODO: Preload the first segment of a video.
}
}
Controlla anche l'esempio di informazioni di rete per scoprire come reagire alle modifiche della rete.
Prememorizzare nella cache più primi segmenti
E se volessi precaricare in modo speculativo alcuni contenuti multimediali
senza sapere quale video sceglierà l'utente? Se l'utente si trova su una
pagina web che contiene 10 video, probabilmente abbiamo abbastanza memoria per recuperare un
file di segmento da ciascuno, ma decisamente non dovremmo creare 10 elementi <video>
e 10 oggetti MediaSource
nascosti e iniziare a fornire questi dati.
L'esempio in due parti riportato di seguito mostra come eseguire la pre-cache di più primi segmenti di video utilizzando l'API Cache, potente e facile da usare. Puoi ottenere qualcosa di simile anche
con IndexedDB. Non utilizziamo ancora i worker di servizio perché
l'API Cache è accessibile anche dall'oggetto window
.
Recupera e memorizza nella cache
const videoFileUrls = [
'bat_video_file_1.webm',
'cow_video_file_1.webm',
'dog_video_file_1.webm',
'fox_video_file_1.webm',
];
// Let's create a video pre-cache and store all first segments of videos inside.
window.caches.open('video-pre-cache')
.then(cache => Promise.all(videoFileUrls.map(videoFileUrl => fetchAndCache(videoFileUrl, cache))));
function fetchAndCache(videoFileUrl, cache) {
// Check first if video is in the cache.
return cache.match(videoFileUrl)
.then(cacheResponse => {
// Let's return cached response if video is already in the cache.
if (cacheResponse) {
return cacheResponse;
}
// Otherwise, fetch the video from the network.
return fetch(videoFileUrl)
.then(networkResponse => {
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, networkResponse.clone());
return networkResponse;
});
});
}
Tieni presente che, se dovessi utilizzare le richieste HTTP Range
, dovrei ricreare manualmente un oggetto Response
, poiché l'API Cache non supporta ancora le risposte Range
. Tieni conto che l'utilizzo di networkResponse.arrayBuffer()
recupera tutti i contenuti della risposta contemporaneamente nella memoria del renderer, motivo per cui ti consigliamo di utilizzare intervalli ridotti.
Come riferimento, ho modificato parte dell'esempio riportato sopra per salvare le richieste HTTP Range nella precache del video.
...
return fetch(videoFileUrl, { headers: { range: 'bytes=0-567139' } })
.then(networkResponse => networkResponse.arrayBuffer())
.then(data => {
const response = new Response(data);
// Add the response to the cache and return network response in parallel.
cache.put(videoFileUrl, response.clone());
return response;
});
Riproduci video
Quando un utente fa clic su un pulsante di riproduzione, recuperiamo il primo segmento di video disponibile nell'API Cache in modo che la riproduzione inizi immediatamente, se disponibile. In caso contrario, lo recuperiamo semplicemente dalla rete. Tieni presente che i browser e gli utenti potrebbero decidere di svuotare la cache.
Come abbiamo visto in precedenza, utilizziamo la tecnologia MSE per indirizzare il primo segmento di video all'elemento video.
function onPlayButtonClick(videoFileUrl) {
video.load(); // Used to be able to play video later.
window.caches.open('video-pre-cache')
.then(cache => fetchAndCache(videoFileUrl, cache)) // Defined above.
.then(response => response.arrayBuffer())
.then(data => {
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen, { once: true });
function sourceOpen() {
URL.revokeObjectURL(video.src);
const sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp09.00.10.08"');
sourceBuffer.appendBuffer(data);
video.play().then(() => {
// TODO: Fetch the rest of the video when user starts playing video.
});
}
});
}
Creare risposte di intervallo con un worker di servizio
E se hai recuperato un intero file video e lo hai salvato nell'API Cache? Quando il browser invia una richiesta HTTP Range
, di certo non vorrai caricare l'intero video nella memoria del renderer, in quanto l'API Cache non supporta ancora le risposte Range
.
Vediamo quindi come intercettare queste richieste e restituire una risposta Range
personalizzata da un service worker.
addEventListener('fetch', event => {
event.respondWith(loadFromCacheOrFetch(event.request));
});
function loadFromCacheOrFetch(request) {
// Search through all available caches for this request.
return caches.match(request)
.then(response => {
// Fetch from network if it's not already in the cache.
if (!response) {
return fetch(request);
// Note that we may want to add the response to the cache and return
// network response in parallel as well.
}
// Browser sends a HTTP Range request. Let's provide one reconstructed
// manually from the cache.
if (request.headers.has('range')) {
return response.blob()
.then(data => {
// Get start position from Range request header.
const pos = Number(/^bytes\=(\d+)\-/g.exec(request.headers.get('range'))[1]);
const options = {
status: 206,
statusText: 'Partial Content',
headers: response.headers
}
const slicedResponse = new Response(data.slice(pos), options);
slicedResponse.setHeaders('Content-Range': 'bytes ' + pos + '-' +
(data.size - 1) + '/' + data.size);
slicedResponse.setHeaders('X-From-Cache': 'true');
return slicedResponse;
});
}
return response;
}
}
È importante notare che ho utilizzato response.blob()
per ricreare questa risposta dettagliata, in quanto questo mi consente di gestire il file, mentre response.arrayBuffer()
porta l'intero file nella memoria del renderer.
La mia intestazione HTTP X-From-Cache
personalizzata può essere utilizzata per sapere se questa richiesta proviene dalla cache o dalla rete. Può essere utilizzato da un player come
ShakaPlayer per ignorare il tempo di risposta come indicatore della
velocità della rete.
Dai un'occhiata all'App Media di esempio ufficiale, in particolare al suo file ranged-response.js, per una soluzione completa su come gestire le richieste Range
.