Schnelle Wiedergabe durch Vorabladen von Audio- und Videoinhalten

Medienwiedergabe durch aktives Vorabladen von Ressourcen beschleunigen

François Beaufort
François Beaufort

Eine schnellere Wiedergabe bedeutet, dass mehr Nutzer Ihr Video ansehen oder Ihre Audioinhalte anhören. Das ist eine bekannte Tatsache. In diesem Artikel geht es um Techniken, mit denen Sie die Audio- und Videowiedergabe beschleunigen können, indem Sie je nach Anwendungsfall Ressourcen aktiv vorab laden.

Bildnachweis: Copyright Blender Foundation | www.blender.org .

Im Folgenden beschreiben wir drei Methoden zum Vorabladen von Mediendateien und welche Vor- und Nachteile sie haben.

Großartig... Aber ...
Attribut „Video preload“ Einfach zu verwenden für eine eindeutige Datei, die auf einem Webserver gehostet wird. Browser ignorieren das Attribut möglicherweise vollständig.
Der Ressourcenabruf beginnt, wenn das HTML-Dokument vollständig geladen und geparst wurde.
Media Source Extensions (MSE) ignorieren das Attribut preload für Medienelemente, da die Anwendung für die Bereitstellung von Medien für MSE verantwortlich ist.
Vorabladen von Links Erzwingt den Browser, eine Videoressource anzufordern, ohne das onload-Ereignis des Dokuments zu blockieren. HTTP-Bereichsanfragen sind nicht kompatibel.
Kompatibel mit MSE und Dateisegmenten. Sollte beim Abrufen aller Ressourcen nur für kleine Mediendateien (<5 MB) verwendet werden.
Manuelle Zwischenspeicherung Uneingeschränkter Zugriff Die Website ist für die Verarbeitung komplexer Fehler verantwortlich.

Attribut „Vorabladen des Videos“

Wenn die Videoquelle eine eindeutige Datei ist, die auf einem Webserver gehostet wird, können Sie das Videoattribut preload verwenden, um dem Browser einen Hinweis darauf zu geben, wie viele Informationen oder Inhalte vorab geladen werden sollen. Das bedeutet, dass Media Source Extensions (MSE) nicht mit preload kompatibel sind.

Das Abrufen von Ressourcen beginnt erst, wenn das ursprüngliche HTML-Dokument vollständig geladen und geparst wurde (z. B. das Ereignis DOMContentLoaded). Das ganz andere load-Ereignis wird ausgelöst, wenn die Ressource tatsächlich abgerufen wurde.

Wenn das Attribut preload auf metadata gesetzt ist, wird davon ausgegangen, dass der Nutzer das Video nicht benötigt. Stattdessen sollten aber die Metadaten (Dimensionen, Titelliste, Dauer usw.) abgerufen werden. Ab Chrome 64 ist der Standardwert für preload metadata. (Vorher waren es 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>

Wenn das Attribut preload auf auto gesetzt ist, kann der Browser genügend Daten im Cache speichern, sodass eine vollständige Wiedergabe ohne Stopp zur weiteren Zwischenspeicherung möglich ist.

<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>

Es gibt jedoch einige Einschränkungen. Da dies nur ein Hinweis ist, kann es sein, dass der Browser das Attribut preload vollständig ignoriert. Zum Zeitpunkt der Erstellung dieses Artikels wurden einige Regeln in Chrome angewendet:

  • Wenn der Datensparmodus aktiviert ist, erzwingt Chrome den preload-Wert auf none.
  • In Android 4.3 erzwingt Chrome den preload-Wert aufgrund eines Android-Fehlers auf none.
  • Bei einer Mobilfunkverbindung (2G, 3G und 4G) erzwingt Chrome den preload-Wert auf metadata.

Tipps

Wenn Ihre Website viele Videoressourcen in derselben Domain enthält, empfehlen wir, den Wert preload auf metadata zu setzen oder das Attribut poster zu definieren und preload auf none zu setzen. Auf diese Weise vermeiden Sie, dass die maximale Anzahl von HTTP-Verbindungen zur selben Domain (6 gemäß der HTTP 1.1-Spezifikation) erreicht wird, die das Laden von Ressourcen hängen könnten. Beachte, dass sich dadurch auch die Seitengeschwindigkeit verbessern kann, wenn Videos nicht Teil deiner grundlegenden Nutzererfahrung sind.

Wie in anderen Artikeln behandelt, ist das Vorabladen von Links ein deklarativer Abruf, mit dem Sie den Browser dazu zwingen können, eine Anfrage für eine Ressource zu senden, ohne das load-Ereignis zu blockieren, und während die Seite heruntergeladen wird. Über <link rel="preload"> geladene Ressourcen werden lokal im Browser gespeichert und sind so lange inaktiv, bis sie im DOM, JavaScript oder CSS explizit referenziert werden.

Das Vorabladen unterscheidet sich vom Prefetch insofern, als er sich auf die aktuelle Navigation konzentriert und Ressourcen basierend auf ihrem Typ (Skript, Stil, Schriftart, Video, Audio usw.) mit Priorität abruft. Sie sollte verwendet werden, um den Browser-Cache für aktuelle Sitzungen aufzuwärmen.

Vollständiges Video vorab laden

So kannst du ein vollständiges Video vorab auf deine Website laden. Wenn dein JavaScript-Code den Videoinhalt abrufen möchte, wird er aus dem Cache gelesen, da die Ressource möglicherweise bereits vom Browser im Cache gespeichert wurde. Wenn die Vorabladeanfrage noch nicht abgeschlossen ist, erfolgt ein normaler Netzwerkabruf.

<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>

Da die vorab geladene Ressource im Beispiel von einem Videoelement genutzt wird, lautet der Wert des vorab geladenen Links as video. Wenn es ein Audioelement wäre, wäre es as="audio".

Das erste Segment vorab laden

Im folgenden Beispiel wird gezeigt, wie das erste Segment eines Videos mit <link rel="preload"> vorab geladen und mit Medienquellenerweiterungen verwendet wird. Wenn du mit der MSE JavaScript API nicht vertraut bist, lies den Artikel MSE-Grundlagen.

Nehmen wir der Einfachheit halber an, dass das gesamte Video in kleinere Dateien wie file_1.webm, file_2.webm, file_3.webm usw. aufgeteilt wurde.

<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>

Support

Mithilfe der folgenden Snippets können Sie die Unterstützung verschiedener as-Typen für <link rel=preload> ermitteln:

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

Manuelle Zwischenspeicherung

Bevor wir uns mit der Cache API und Service Workern befassen, sehen wir uns an, wie ein Video mit MSE manuell zwischengespeichert wird. Im folgenden Beispiel wird davon ausgegangen, dass Ihr Webserver HTTP-Range-Anfragen unterstützt. Bei Dateisegmenten würde das aber ziemlich ähnlich sein. Einige Middleware-Bibliotheken wie der Shaka Player von Google, der JW Player und Video.js sind so konzipiert, dass dies für Sie erledigt wird.

<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>

Wissenswertes

Da Sie nun die Kontrolle über die Zwischenspeicherung von Medien haben, sollten Sie beim Vorabladen auf den Akkustand des Geräts, die Nutzereinstellung des Datensparmodus und die Netzwerkinformationen achten.

Akkubewusstsein

Berücksichtige den Akkustand der Geräte der Nutzer, bevor du ein Video vorab lädst. Dadurch wird die Akkulaufzeit bei niedrigem Akkustand verlängert.

Deaktivieren Sie das Vorabladen oder laden Sie zumindest ein Video mit geringerer Auflösung vorab, wenn der Akku des Geräts leer ist.

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

„Datensparmodus“ erkennen

Verwenden Sie den Save-Data-Clienthinweis-Anfrageheader, um schnelle und einfache Anwendungen für Nutzer bereitzustellen, die in ihrem Browser den „Datensparmodus“ aktiviert haben. Durch Identifizierung dieses Anfrageheaders kann Ihre Anwendung Nutzern mit kosten- und leistungsbeschränkten Nutzern eine optimale Nutzererfahrung bieten.

Weitere Informationen finden Sie unter Delivering Fast and Small Applications with Save Data.

Intelligentes Laden auf Grundlage von Netzwerkinformationen

Du solltest navigator.connection.type vor dem Vorabladen prüfen. Wenn cellular festgelegt ist, können Sie das Vorabladen verhindern und Nutzer darauf hinweisen, dass ihr Mobilfunkanbieter möglicherweise die Bandbreite berechnet und nur die automatische Wiedergabe von zuvor im Cache gespeicherten Inhalten startet.

if ('connection' in navigator) {
  if (navigator.connection.type == 'cellular') {
    // TODO: Prompt user before preloading video
  } else {
    // TODO: Preload the first segment of a video.
  }
}

Im Beispiel für die Netzwerkinformationen sehen Sie, wie Sie auch auf Netzwerkänderungen reagieren können.

Mehrere erste Segmente vorab im Cache speichern

Was ist, wenn ich Medieninhalte spekulativ vorab laden möchte, ohne zu wissen, welches Medium der Nutzer letztendlich auswählen wird? Wenn sich der Nutzer auf einer Webseite mit 10 Videos befindet, haben wir wahrscheinlich genügend Speicher, um von jedem eine Segmentdatei abzurufen. Wir sollten aber auf keinen Fall 10 versteckte <video>-Elemente und 10 MediaSource-Objekte erstellen und mit der Eingabe dieser Daten beginnen.

Das zweiteilige Beispiel unten zeigt, wie Sie mit der leistungsstarken und nutzerfreundlichen Cache API mehrere erste Segmente eines Videos vorab im Cache speichern können. Ähnliches kann auch mit IndexedDB erreicht werden. Wir verwenden noch keine Service Worker, da über das Objekt window auch auf die Cache API zugegriffen werden kann.

Abrufen und im Cache speichern

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

Wenn ich HTTP-Range-Anfragen verwenden würde, müsste ich ein Response-Objekt manuell neu erstellen, da die Cache API noch keine Range-Antworten unterstützt. Wenn Sie networkResponse.arrayBuffer() aufrufen, wird der gesamte Inhalt der Antwort auf einmal im Arbeitsspeicher des Renderers abgerufen. Daher empfiehlt es sich, kleine Bereiche zu verwenden.

Ich habe einen Teil des Beispiels oben so geändert, dass HTTP-Bereichsanfragen im Video-Vorcache gespeichert werden.

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

Video abspielen

Wenn ein Nutzer auf eine Wiedergabeschaltfläche klickt, wird das erste Segment des Videos abgerufen, das in der Cache API verfügbar ist. Die Wiedergabe wird dann, sofern verfügbar, sofort gestartet. Andernfalls rufen wir sie einfach aus dem Netzwerk ab. Browser und Nutzer können den Cache leeren.

Wie bereits erwähnt, verwenden wir MSE, um das erste Segment des Videos dem Videoelement zu übertragen.

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

Bereichsantworten mit einem Service Worker erstellen

Was passiert, wenn Sie eine vollständige Videodatei abgerufen und in der Cache API gespeichert haben? Wenn der Browser eine HTTP-Range-Anfrage sendet, sollten Sie auf keinen Fall das gesamte Video in den Arbeitsspeicher des Renderers übertragen, da die Cache API noch keine Range-Antworten unterstützt.

Ich zeige Ihnen nun, wie diese Anfragen abgefangen und eine benutzerdefinierte Range-Antwort von einem Service Worker zurückgegeben werden kann.

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

Wichtig: Ich habe response.blob() verwendet, um diese aufgeteilte Antwort neu zu erstellen. Das gibt mir einfach einen Handle für die Datei, während response.arrayBuffer() die gesamte Datei im Arbeitsspeicher des Renderers bereitstellt.

Mit meinem benutzerdefinierten HTTP-Header X-From-Cache kann ich feststellen, ob diese Anfrage aus dem Cache oder aus dem Netzwerk stammt. Es kann von einem Spieler wie ShakaPlayer verwendet werden, um die Antwortzeit als Indikator für die Netzwerkgeschwindigkeit zu ignorieren.

In der offiziellen Beispielmedien-App und insbesondere in der Datei ranged-response.js finden Sie eine Komplettlösung zum Verarbeiten von Range-Anfragen.