Riproduzione veloce con precaricamento audio e video

Scopri come accelerare la riproduzione dei contenuti multimediali precaricando attivamente le risorse.

Francesco Beaufort
François Beaufort

Un avvio più rapido della riproduzione significa che più persone guardano il tuo video o ascoltano il tuo audio. È un fatto risaputo. In questo articolo esploreremo le tecniche che puoi utilizzare per accelerare la riproduzione audio e video precaricando attivamente le risorse in base al caso d'uso.

Ringraziamenti: copyright Blender Foundation | www.blender.org .

Descriverò tre metodi di precaricamento dei file multimediali, iniziando dai loro pro e i loro contro.

È fantastico... Ma…
Attributo di precaricamento video È semplice da utilizzare per un file univoco ospitato su un server web. I browser potrebbero ignorare completamente l'attributo.
Il recupero della risorsa inizia quando il documento HTML è stato completamente caricato e analizzato.
Le estensioni Media Source Extensions (MSE) ignorano l'attributo preload negli elementi multimediali perché l'app è responsabile della fornitura di contenuti multimediali a MSE.
Precaricamento link Obbliga il browser a effettuare una richiesta per una risorsa video senza bloccare l'evento onload del documento. Le richieste di intervallo HTTP non sono compatibili.
Compatibile con MSE e segmenti di file. Da utilizzare solo per file multimediali di piccole dimensioni (< 5 MB) durante il recupero delle risorse complete.
buffering manuale Controllo totale La gestione degli errori complessi è responsabilità del sito web.

Attributo di precaricamento video

Se l'origine del video è un file univoco ospitato su un server web, potresti voler utilizzare l'attributo video preload per fornire un suggerimento al browser su quante informazioni o contenuti precaricare. Ciò significa che Media Source Extensions (MSE) non è compatibile con preload.

Il recupero delle risorse inizia solo dopo che il documento HTML iniziale è stato completamente caricato e analizzato (ad esempio, è stato attivato l'evento DOMContentLoaded), mentre l'evento load molto diverso viene attivato quando la risorsa è stata effettivamente recuperata.

L'impostazione dell'attributo preload su metadata indica che all'utente non dovrebbe essere necessario il video, ma è preferibile recuperarne i metadati (dimensioni, elenco delle 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>

L'impostazione dell'attributo preload su auto indica che il browser potrebbe memorizzare nella cache una quantità sufficiente di dati che consentono di completare la riproduzione senza necessità di interruzione per ulteriori 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 necessario tenere conto di alcuni aspetti. Poiché questo è solo un suggerimento, il browser potrebbe ignorare completamente l'attributo preload. Al momento della scrittura, ecco alcune regole applicate in Chrome:

  • Quando l'opzione Risparmio dati è attiva, Chrome forza il valore preload a none.
  • In Android 4.3, Chrome forza il valore preload a none a causa di un Bug Android.
  • Su una connessione cellulare (2G, 3G e 4G), Chrome forza il valore preload a metadata.

Suggerimenti

Se il tuo sito web contiene molte risorse video nello stesso dominio, ti consiglio di impostare il valore preload su metadata o di definire l'attributo poster e di impostare preload su none. In questo modo, eviteresti di raggiungere il numero massimo di connessioni HTTP allo stesso dominio (6 in base alla specifica HTTP 1.1), che potrebbe bloccare il caricamento delle risorse. Tieni presente che ciò potrebbe anche migliorare la velocità delle pagine se i video non fanno parte della tua esperienza utente principale.

Come spiegato 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 efficaci inerti fino a quando 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). Da utilizzare per scaldare 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 JavaScript richiede il recupero dei contenuti video, venga letto dalla cache, in quanto la risorsa potrebbe essere già stata memorizzata nella cache dal browser. Se la richiesta di precaricamento non è ancora stata completata, verrà eseguito un normale recupero della 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 come utilizzarlo con Media Source Extensions. Se non hai dimestichezza con l'API MSE JavaScript, consulta le nozioni di base su MSE.

Per semplicità, supponiamo che l'intero video sia stato suddiviso in file più piccoli come 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> utilizzando 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 addentrarci nell'API Cache e sui service worker, vediamo come eseguire manualmente il buffering di un video con MSE. L'esempio riportato di seguito presuppone che il tuo server web supporti le richieste HTTP Range, ma l'operazione 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 problema.

<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

Poiché ora hai il controllo dell'intera esperienza di buffering dei contenuti multimediali, ti consiglio di prendere in considerazione il livello della batteria del dispositivo, la preferenza utente "Modalità risparmio dati" e le informazioni di rete quando pensi al precaricamento.

Conoscenza della batteria

Prendi in considerazione il livello di carica della batteria dei dispositivi degli utenti prima di precaricare un video. Ciò consente di preservare la durata della batteria quando il livello di carica è basso.

Disattiva il precaricamento o almeno precarica un video a risoluzione più bassa quando la batteria del dispositivo è in esaurimento.

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

Rilevamento del "Risparmio dati"

Utilizza l'intestazione della richiesta del 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, l'applicazione può personalizzare e offrire un'esperienza utente ottimizzata per gli utenti con limiti di prestazioni e costi.

Per saperne di più, consulta l'articolo Distribuzione di applicazioni veloci e leggere con la funzionalità Save-Data.

Caricamento intelligente basato sulle informazioni di rete

Ti consigliamo di controllare navigator.connection.type prima del precaricamento. Se l'opzione è impostata su cellular, puoi impedire il precaricamento e comunicare agli utenti che il loro operatore di rete mobile potrebbe addebitare costi per la larghezza di banda e avviare solo la riproduzione automatica dei contenuti precedentemente memorizzati 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.
  }
}

Consulta l'esempio di informazioni sulla rete per scoprire anche come reagire alle modifiche della rete.

Pre-memorizzazione nella cache più primi segmenti

E se volessi precaricare in modo speculativo dei contenuti multimediali senza sapere quale elemento multimediale verrà scelto dall'utente? Se l'utente si trova in una pagina web che contiene 10 video, probabilmente la nostra memoria è sufficiente per recuperare un file di segmento da ciascun video, ma non dovremmo creare 10 elementi <video> nascosti e 10 oggetti MediaSource e iniziare a fornire questi dati.

L'esempio in due parti riportato di seguito mostra come pre-memorizzare nella cache più primi segmenti di video utilizzando l'efficace e semplice API Cache. Tieni presente che anche IndexedDB può ottenere un risultato simile. Non stiamo ancora utilizzando i service worker perché l'API Cache è accessibile anche dall'oggetto window.

Recupero e 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. Ricorda che la chiamata a networkResponse.arrayBuffer() recupera l'intero contenuto della risposta contemporaneamente nella memoria del renderer, motivo per cui è consigliabile utilizzare intervalli ridotti.

Come riferimento, ho modificato parte dell'esempio sopra per salvare le richieste di intervallo HTTP 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 il 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 venga avviata immediatamente, se disponibile. In caso contrario, lo recupereremo semplicemente dalla rete. Tieni presente che i browser e gli utenti possono decidere di svuotare la cache.

Come abbiamo visto prima, utilizziamo MSE per inviare 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.
      });
    }
  });
}

Creazione di risposte a intervalli con un service worker

E se hai recuperato un intero file video e lo hai salvato nell'API Cache? Quando il browser invia una richiesta Range HTTP, non è assolutamente necessario inserire l'intero video nella memoria del renderer perché l'API Cache non supporta ancora le risposte Range.

Vediamo 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 frammentata, dato che questo mi fornisce semplicemente un handle per il file, mentre response.arrayBuffer() sposta 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.

Consulta l'app Sample Media ufficiale e in particolare il file ranged-response.js, per una soluzione completa su come gestire le richieste di Range.