Riproduzione veloce con precaricamento audio e video

Come accelerare la riproduzione di contenuti multimediali precaricando attivamente le risorse.

François Beaufort
François Beaufort

Una riproduzione più rapida si traduce in un numero maggiore di persone che guardano il tuo video o ascoltano il tuo audio. È un fatto risaputo. In questo articolo, esplorerai tecniche che puoi utilizzare per accelerare la riproduzione audio e video di precaricamento delle risorse a seconda del caso d'uso.

Crediti: copyright Uniscier Foundation | www.blender.org .

descriverò tre metodi per precaricare i file multimediali, a partire dai loro professionisti e contro.

È magnifico... 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 delle risorse inizia quando il documento HTML è stato completamente caricato analizzato.
Media Source Extensions (MSE) ignora l'attributo preload negli elementi multimediali perché l'app è responsabile la fornitura di contenuti multimediali all'MSE.
Precaricamento link Forza 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) quando recuperi le risorse complete.
Buffering manuale Pieno controllo 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, potresti voler Utilizza l'attributo video preload per fornire al browser un suggerimento su come informazioni o contenuti da precaricare. Ciò significa che Media Source Extensions (MSE) non è compatibile con preload.

Il recupero della risorsa verrà avviato solo quando il documento HTML iniziale è stato completamente caricati e analizzati (ad es. è stato attivato l'evento DOMContentLoaded) mentre un evento load molto diverso verrà attivato quando la risorsa è stato effettivamente recuperato.

Se l'attributo preload è impostato su metadata, l'utente non è dovrebbe richiedere il video, ma questo recupero dei metadati (dimensioni, elenco, durata e così via) è desiderabile. Tieni presente che da Chrome 64, il valore predefinito per preload è metadata. (La durata era auto in precedenza).

<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 è impostato su auto, il browser potrebbe memorizzare nella cache sia possibile ottenere una quantità sufficiente di dati per completare la riproduzione senza dover interrompere 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>

Ci sono però alcune avvertenze. Poiché questo è solo un suggerimento, il browser potrebbe ignora l'attributo preload. Al momento della stesura di questo documento, ecco alcune regole applicati in Chrome:

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

Suggerimenti

Se il tuo sito web contiene molte risorse video sullo stesso dominio, ti consigliamo di impostare il valore preload su metadata o di definire il poster e imposta preload su none. In questo modo, eviteresti di colpire il numero massimo di connessioni HTTP allo stesso dominio (6 in base al HTTP 1.1) che può bloccare il caricamento delle risorse. Tieni presente che anche in questo caso migliorare la velocità delle pagine se i video non rientrano nell'esperienza utente di base.

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 blocca l'evento load e durante il download della pagina. Risorse caricati tramite <link rel="preload"> vengono memorizzati localmente nel browser e sono inerziano in modo efficace finché non viene fatto riferimento esplicitamente in DOM, JavaScript o CSS.

Il precaricamento è diverso dal precaricamento perché si concentra sulla navigazione corrente recupera le risorse con priorità in base al tipo (script, stile, carattere video, audio ecc.). Deve essere utilizzata per riscaldare la cache del browser sessioni.

Precarica il video completo

Ecco come precaricare un video completo sul tuo sito web in modo che, quando JavaScript chiede il recupero dei contenuti video, che vengono letti dalla cache come risorsa potrebbero essere già state memorizzate nella cache dal browser. Se la richiesta di precaricamento non è stata è terminato, 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 in Nell'esempio, il valore del link di precaricamento as è video. Se fosse un audio , sarà 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 familiarità 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> con il 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 manualmente il buffering di un video con MSE. Nell'esempio seguente si presuppone che il tuo sito web supporta HTTP Range richieste, ma sarebbe abbastanza simile con le richieste segmenti. Tieni presente che alcune librerie middleware come Shaka di Google Player, JW Player e Video.js sono è progettato per occuparsene.

<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, ti consiglio di tieni in considerazione il livello della batteria del dispositivo, la "modalità Risparmio dati" preferenze utente e le informazioni di rete quando si pensa al precaricamento.

Consapevolezza della batteria

Considera il livello della batteria degli utenti dispositivi prima di pensare sul precaricamento di un video. Questo consente di preservare la durata della batteria quando il livello di alimentazione è basso.

Disattiva o almeno precarica un video a risoluzione più bassa quando la batteria del dispositivo si sta esaurendo.

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 del client Save-Data per una distribuzione rapida e chiara applicazioni agli utenti che hanno attivato il "risparmio di dati" nei suoi del browser. Se identifichi questa intestazione della richiesta, la tua applicazione può personalizzare offrire un'esperienza utente ottimizzata a limiti di costi e prestazioni utenti.

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. Quando è impostato su cellular, potresti impedire il precaricamento e consigliare agli utenti che l'operatore di rete mobile potrebbe addebitare la larghezza di banda e iniziare la riproduzione automatica di 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.
  }
}

Guarda l'esempio di informazioni di rete per scoprire come reagire alla rete modifiche.

Prememorizzare nella cache più primi segmenti

E se volessi precaricare in modo speculativo alcuni contenuti multimediali senza sapere quali contenuti multimediali sceglierà l'utente? Se l'utente si trova su un pagina web contenente 10 video, probabilmente abbiamo memoria sufficiente per recuperarne uno del segmento di pubblico di ogni dominio, ma di sicuro non dovremmo creare 10 <video> nascosti e 10 MediaSource oggetti e iniziare a inserire questi dati.

L'esempio in due parti riportato di seguito mostra come prememorizzare nella cache più primi segmenti di utilizzando l'API Cache, potente e facile da usare. Tieni presente che qualcosa di simile possono essere ottenuti anche con IndexedDB. Non utilizziamo ancora i service worker come 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 utilizzassi le richieste Range HTTP, dovrei ricreare manualmente un oggetto Response poiché l'API Cache non supporta ancora le risposte Range. Essere ricordando che la chiamata a networkResponse.arrayBuffer() consente di recuperare l'intero contenuto della risposta nella memoria del renderer; per questo conviene usare piccoli intervalli.

Come riferimento, ho modificato parte dell'esempio precedente per salvare l'intervallo HTTP richieste alla preregistrazione 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 del video disponibile nell'API Cache in modo che la riproduzione inizi immediatamente, se disponibile. Altrimenti, lo recuperiamo semplicemente dalla rete. Ricorda che i browser e gli utenti possono decidere di svuotare la cache.

Come abbiamo visto in precedenza, utilizziamo la tecnologia MSE per indirizzare il primo segmento del video al 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 a intervalli con un service worker

Che cosa succede se hai recuperato un intero file video e lo hai salvato l'API Cache? Quando il browser invia una richiesta Range HTTP, vuoi portare l'intero video nella memoria del renderer perché l'API Cache non supporta Range risposte ancora.

Vediamo quindi come intercettare queste richieste e restituire un'istruzione Range personalizzata una risposta 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 sezione come risposta, poiché mi consente di gestire il file mentre response.arrayBuffer() inserisce l'intero file nella memoria del renderer.

La mia intestazione HTTP X-From-Cache personalizzata può essere utilizzata per sapere se questa richiesta provengono dalla cache o dalla rete. Può essere utilizzato da un player come ShakaPlayer per ignorare il tempo di risposta come indicatore di la velocità della rete.

Dai un'occhiata all'app multimediale di esempio ufficiale, in particolare alle sue ranged-response.js per una soluzione completa su come gestire Range richieste.