So beschleunigen Sie die Medienwiedergabe durch aktives Vorladen von Ressourcen.
Je schneller die Wiedergabe gestartet wird, desto mehr Nutzer sehen sich dein Video an oder hören sich deine Audioinhalte an. Das ist eine bekannte Tatsache. In diesem Artikel stelle ich Ihnen Methoden vor, mit denen Sie die Audio- und Videowiedergabe beschleunigen können, indem Sie je nach Anwendungsfall Ressourcen aktiv vorladen.
Ich beschreibe drei Methoden zum Vorabladen von Mediendateien, beginnend mit ihren Vor- und Nachteilen.
Das ist super… | Aber... | |
---|---|---|
Attribut „Video-Preload“ | Einfache Verwendung für eine eindeutige Datei, die auf einem Webserver gehostet wird. | Browser ignorieren das Attribut möglicherweise vollständig. |
Das Abrufen von Ressourcen beginnt, wenn das HTML-Dokument vollständig geladen und geparst wurde. | ||
Bei Media Source Extensions (MSE) wird das Attribut preload für Medienelemente ignoriert, da die App für die Bereitstellung von Medien für MSE verantwortlich ist.
|
||
Link-Preload |
Der Browser wird gezwungen, eine Anfrage für eine Videoressource zu senden, ohne das Ereignis onload des Dokuments zu blockieren.
|
HTTP-Bereichsanfragen sind nicht kompatibel. |
Kompatibel mit MSE und Dateisegmenten. | Sollte nur für kleine Mediendateien (< 5 MB) verwendet werden, wenn vollständige Ressourcen abgerufen werden. | |
Manuelles Puffern | Uneingeschränkter Zugriff | Die komplexe Fehlerbehandlung liegt in der Verantwortung der Website. |
Attribut „Video-Preload“
Wenn die Videoquelle eine einzelne Datei ist, die auf einem Webserver gehostet wird, können Sie mit dem Attribut „video“ preload
dem Browser einen Hinweis geben, wie viele Informationen oder Inhalte vorab geladen werden sollen. Das bedeutet, dass Media Source Extensions (MSE) nicht mit preload
kompatibel ist.
Das Abrufen der Ressource beginnt erst, wenn das ursprüngliche HTML-Dokument vollständig geladen und geparst wurde (z. B. wenn das Ereignis DOMContentLoaded
ausgelöst wurde). Das Ereignis load
wird dagegen ausgelöst, wenn die Ressource tatsächlich abgerufen wurde.
Wenn du das Attribut preload
auf metadata
festlegst, wird damit angegeben, dass der Nutzer das Video zwar nicht benötigt, aber das Abrufen seiner Metadaten (Dimensionen, Titelliste, Dauer usw.) wünschenswert ist. Hinweis: Ab Chrome 64 ist der Standardwert für preload
metadata
. Bisher war 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 du das preload
-Attribut auf auto
setzt, kann der Browser genügend Daten im Cache speichern, damit die Wiedergabe vollständig erfolgen kann, ohne dass die Wiedergabe für eine weitere Pufferung pausiert werden muss.
<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 der Browser das Attribut preload
vollständig ignorieren. Hier sind einige Regeln, die derzeit in Chrome gelten:
- Wenn der Datensparmodus aktiviert ist, wird in Chrome der Wert für
preload
aufnone
festgelegt. - Unter Android 4.3 setzt Chrome den Wert für
preload
aufnone
, da es einen Android-Bug gibt. - Bei einer Mobilfunkverbindung (2G, 3G und 4G) setzt Chrome den Wert für
preload
aufmetadata
.
Tipps
Wenn Ihre Website viele Videoressourcen auf derselben Domain enthält, sollten Sie den Wert für preload
auf metadata
festlegen oder das Attribut poster
definieren und preload
auf none
setzen. So wird verhindert, dass die maximale Anzahl von HTTP-Verbindungen zur selben Domain erreicht wird (6 gemäß der HTTP 1.1-Spezifikation), was das Laden von Ressourcen blockieren kann. Dies kann auch die Seitenladegeschwindigkeit verbessern, wenn Videos nicht Teil der Hauptfunktionen Ihrer Website sind.
Vorabladen von Links
Wie in anderen Artikeln erläutert, ist das Vorladen von Links ein deklarativer Abruf, mit dem Sie den Browser zwingen können, eine Ressource anzufordern, 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 inaktiv, bis sie im DOM, in JavaScript oder in CSS explizit referenziert werden.
Das Vorladen unterscheidet sich vom Vorabladen dadurch, dass es sich auf die aktuelle Navigation konzentriert und Ressourcen priorisiert nach ihrem Typ abholt (Script, Stil, Schriftart, Video, Audio usw.). Sie sollte verwendet werden, um den Browsercache für aktuelle Sitzungen zu beschleunigen.
Vollständiges Video vorab laden
So laden Sie ein vollständiges Video auf Ihrer Website vor, damit es aus dem Cache gelesen wird, wenn Ihr JavaScript-Code den Abruf von Videoinhalten anfordert. Die Ressource wurde möglicherweise bereits vom Browser im Cache gespeichert. Wenn die Vorab-Anfrage 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 in diesem Beispiel von einem Videoelement verwendet wird, ist der Wert für den as
-Link zum Vorladen video
. Bei einem Audioelement wäre das as="audio"
.
Erstes Segment vorab laden
Im folgenden Beispiel wird gezeigt, wie du das erste Segment eines Videos mit <link
rel="preload">
vorlädst und es mit Media Source Extensions verwendest. Wenn du mit der MSE JavaScript API nicht vertraut bist, lies den Hilfeartikel MSE-Grundlagen.
Angenommen, das gesamte Video wurde in kleinere Dateien wie file_1.webm
, file_2.webm
und file_3.webm
aufgeteilt.
<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
Mit den folgenden Snippets kannst du die Unterstützung verschiedener as
-Typen für <link rel=preload>
prüfen:
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');
}
Manuelles Puffern
Bevor wir uns mit der Cache API und Service Workern befassen, sehen wir uns an, wie Sie ein Video mit MSE manuell puffern. Im folgenden Beispiel wird davon ausgegangen, dass dein Webserver HTTP-Range
-Anfragen unterstützt. Das würde aber auch mit Dateisegmenten ähnlich aussehen. Einige Middleware-Bibliotheken wie der Shaka Player von Google, der JW Player und Video.js sind dafür konzipiert.
<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>
Hinweise
Da Sie jetzt die gesamte Medienpufferung steuern, sollten Sie beim Vorabladen den Akkustand des Geräts, die Nutzereinstellung „Datensparmodus“ und die Netzwerkinformationen berücksichtigen.
Akkustatus
Berücksichtigen Sie den Akkustand der Geräte der Nutzer, bevor Sie ein Video vorab laden. So wird die Akkulaufzeit bei niedrigem Akkustand verlängert.
Deaktivieren Sie das Vorabladen oder laden Sie zumindest ein Video mit niedrigerer Auflösung vor, wenn der Akku des Geräts schwach 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
-Client-Hinweis-Anfrageheader, um Nutzern, die in ihrem Browser den Modus „Datensparmodus“ aktiviert haben, schnelle und schlanke Anwendungen bereitzustellen. Wenn Ihre Anwendung diesen Anfrageheader erkennt, kann sie für Nutzer mit Kosten- und Leistungseinschränkungen angepasst werden und eine optimierte Nutzererfahrung bieten.
Weitere Informationen finden Sie unter Schnelle und schlanke Apps mit dem Data Saver-Dienst bereitstellen.
Intelligentes Laden basierend auf Netzwerkinformationen
Sie sollten navigator.connection.type
vor dem Vorabladen prüfen. Wenn die Einstellung auf cellular
festgelegt ist, kannst du das Vorladen verhindern und Nutzer darauf hinweisen, dass ihr Mobilfunkanbieter möglicherweise Gebühren für die Bandbreite erhebt. Außerdem wird die automatische Wiedergabe nur für zuvor im Cache gespeicherte Inhalte gestartet.
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 Netzwerkinformationen erfahren Sie, wie Sie auf Netzwerkänderungen reagieren.
Mehrere erste Segmente vorab im Cache speichern
Was ist, wenn ich einige Medieninhalte vorab laden möchte, ohne zu wissen, welche Medien der Nutzer letztendlich auswählt? Wenn sich der Nutzer auf einer Webseite mit 10 Videos befindet, haben wir wahrscheinlich genügend Arbeitsspeicher, um jeweils eine Segmentdatei abzurufen. Wir sollten jedoch auf keinen Fall 10 versteckte <video>
-Elemente und 10 MediaSource
-Objekte erstellen und diese Daten einspeisen.
Im zweiteiligen Beispiel unten wird gezeigt, wie du mit der leistungsstarken und einfach zu bedienenden Cache API mehrere erste Videosegmente im Voraus im Cache ablegst. Beachten Sie, dass sich mit IndexedDB ähnliche Ergebnisse erzielen lassen. Wir verwenden noch keine Dienst-Worker, da die Cache API auch über das window
-Objekt zugänglich ist.
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 Range
-Antworten noch nicht unterstützt. Beachte, dass beim Aufrufen von networkResponse.arrayBuffer()
der gesamte Inhalt der Antwort auf einmal in den Renderer-Speicher abgerufen wird. Daher solltest du kleine Bereiche verwenden.
Zur Veranschaulichung habe ich einen Teil des Beispiels oben so geändert, dass HTTP-Bereichsanfragen im Video-Precache 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 Videosegment abgerufen, das in der Cache API verfügbar ist, damit die Wiedergabe sofort beginnt, sofern verfügbar. Andernfalls holen wir sie einfach aus dem Netzwerk ab. Beachten Sie, dass Browser und Nutzer den Cache leeren können.
Wie bereits erwähnt, verwenden wir MSE, um dieses erste Videosegment an das Videoelement weiterzuleiten.
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 ist, wenn du eine ganze Videodatei abgerufen und in der Cache API gespeichert hast? Wenn der Browser eine HTTP-Range
-Anfrage sendet, solltest du das gesamte Video nicht in den Renderer-Speicher aufnehmen, da die Cache API Range
-Antworten noch nicht unterstützt.
Ich zeige Ihnen jetzt, wie Sie diese Anfragen abfangen und eine benutzerdefinierte Range
-Antwort von einem Service Worker zurückgeben.
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;
}
}
Ich habe response.blob()
verwendet, um diese geslicete Antwort neu zu erstellen, da ich damit einfach einen Handle für die Datei erhalte, während response.arrayBuffer()
die gesamte Datei in den Renderer-Speicher bringt.
Anhand meines benutzerdefinierten X-From-Cache
-HTTP-Headers kann ich feststellen, ob diese Anfrage aus dem Cache oder aus dem Netzwerk stammt. Sie kann von einem Player wie ShakaPlayer verwendet werden, um die Antwortzeit als Indikator für die Netzwerkgeschwindigkeit zu ignorieren.
In der offiziellen Sample Media App und insbesondere in der Datei ranged-response.js findest du eine vollständige Lösung für die Verarbeitung von Range
-Anfragen.