Cómo acelerar la reproducción de contenido multimedia mediante la carga previa activa de recursos
Si la reproducción comienza más rápido, más personas mirarán tu video o escucharán tu audio. Eso es un hecho conocido. En este artículo, exploraré las técnicas que puedes usar para acelerar la reproducción de audio y video mediante la carga previa activa de recursos según tu caso de uso.
Describiré tres métodos para precargar archivos multimedia, comenzando con sus ventajas y desventajas.
Es genial… | Pero… | |
---|---|---|
Atributo de carga previa de video | Es fácil de usar para un archivo único alojado en un servidor web. | Los navegadores pueden ignorar el atributo por completo. |
La recuperación de recursos comienza cuando se completa la carga y el análisis del documento HTML. | ||
Las extensiones de fuente de contenido multimedia (MSE) ignoran el atributo preload en los elementos multimedia porque la app es responsable de proporcionar contenido multimedia a MSE.
|
||
Precarga de vínculos |
Fuerza al navegador a realizar una solicitud de un recurso de video sin bloquear el evento onload del documento.
|
Las solicitudes de rango HTTP no son compatibles. |
Compatible con MSE y segmentos de archivos. | Se debe usar solo para archivos multimedia pequeños (<5 MB) cuando se recuperan recursos completos. | |
Almacenamiento en búfer manual | Control total | La responsabilidad de la administración de errores complejos es del sitio web. |
Atributo de carga previa de video
Si la fuente de video es un archivo único alojado en un servidor web, te recomendamos que uses el atributo preload
del video para indicarle al navegador cuanta información o contenido debe cargar previamente. Esto significa que las Extensiones de fuente de medios (MSE) no son compatibles con preload
.
La recuperación de recursos comenzará solo cuando el documento HTML inicial se haya cargado y analizado por completo (p.ej., se haya activado el evento DOMContentLoaded
), mientras que el evento load
muy diferente se activará cuando se haya recuperado el recurso.
Configurar el atributo preload
en metadata
indica que no se espera que el usuario necesite el video, pero que es conveniente recuperar sus metadatos (dimensiones, lista de pistas, duración, etcétera). Ten en cuenta que, a partir de Chrome 64, el valor predeterminado de preload
es metadata
. (Antes 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>
Establecer el atributo preload
en auto
indica que el navegador puede almacenar en caché suficientes datos para que se pueda completar la reproducción sin necesidad de detener el almacenamiento en búfer adicional.
<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>
Sin embargo, hay algunas salvedades. Como esta es solo una sugerencia, el navegador puede ignorar por completo el atributo preload
. En el momento de escribir este artículo, estas son algunas de las reglas que se aplican en Chrome:
- Cuando se habilita el Ahorro de datos, Chrome fuerza el valor
preload
anone
. - En Android 4.3, Chrome fuerza el valor
preload
anone
debido a un error de Android. - En una conexión celular (2G, 3G y 4G), Chrome fuerza el valor de
preload
ametadata
.
Sugerencias
Si tu sitio web contiene muchos recursos de video en el mismo dominio, te recomendaría que configures el valor de preload
en metadata
o definas el atributo poster
y configures preload
en none
. De esa manera, evitarías alcanzar la cantidad máxima de conexiones HTTP al mismo dominio (6 según la especificación HTTP 1.1), lo que puede suspender la carga de recursos. Ten en cuenta que esto también puede mejorar la velocidad de la página si los videos no forman parte de la experiencia del usuario principal.
Precarga de vínculos
Como se menciona en otros artículos, la carga previa de vínculos es una recuperación declarativa que te permite forzar al navegador a que haga una solicitud de un recurso sin bloquear el evento load
y mientras se descarga la página. Los recursos
que se cargan a través de <link rel="preload">
se almacenan de forma local en el navegador y son
inactivos hasta que se hace referencia a ellos de forma explícita en el DOM, JavaScript
o CSS.
La carga previa es diferente de la carga anticipada en que se enfoca en la navegación actual y recupera recursos con prioridad según su tipo (secuencia de comandos, estilo, fuente, video, audio, etcétera). Se debe usar para activar la caché del navegador para las sesiones actuales.
Carga previa del video completo
A continuación, te mostramos cómo precargar un video completo en tu sitio web para que, cuando tu código JavaScript solicite recuperar contenido de video, se lea desde la caché, ya que es posible que el navegador ya haya almacenado en caché el recurso. Si la solicitud de carga previa aún no finalizó, se realizará una recuperación de red normal.
<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>
Como un elemento de video consumirá el recurso precargado en el ejemplo, el valor del vínculo de carga previa as
es video
. Si fuera un elemento de audio, sería as="audio"
.
Cómo precargar el primer segmento
En el siguiente ejemplo, se muestra cómo precargar el primer segmento de un video con <link
rel="preload">
y usarlo con extensiones de origen multimedia. Si no estás familiarizado con la API de JavaScript de MSE, consulta Aspectos básicos de MSE.
Para simplificar, supongamos que todo el video se dividió en archivos más pequeños, como file_1.webm
, file_2.webm
, file_3.webm
, etcétera.
<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>
Asistencia
Puedes detectar la compatibilidad con varios tipos de as
para <link rel=preload>
con los fragmentos que se indican a continuación:
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');
}
Almacenamiento en búfer manual
Antes de analizar la API de caché y los service workers, veamos cómo almacenar en búfer un video de forma manual con MSE. En el siguiente ejemplo, se supone que tu servidor web admite solicitudes HTTP Range
, pero esto sería bastante similar con los segmentos de archivo. Ten en cuenta que algunas bibliotecas de middleware, como Shaka Player de Google, JW Player y Video.js, están compiladas para controlar esto por ti.
<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>
Consideraciones
Ahora que tienes el control de toda la experiencia de almacenamiento en búfer de contenido multimedia, te sugiero que tengas en cuenta el nivel de batería del dispositivo, la preferencia del usuario "Modo de ahorro de datos" y la información de la red cuando pienses en la carga previa.
Conciencia de la batería
Ten en cuenta el nivel de batería de los dispositivos de los usuarios antes de pensar en precargar un video. Esto conservará la duración de batería cuando el nivel de energía sea bajo.
Inhabilita la carga previa o, al menos, carga previamente un video de resolución más baja cuando la batería del dispositivo esté baja.
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.
}
});
}
Detecta "Ahorro de datos"
Usa el encabezado de solicitud de sugerencia de cliente Save-Data
para entregar aplicaciones rápidas y livianas a los usuarios que habilitaron el modo "ahorro de datos" en su navegador. Si identificas este encabezado de solicitud, tu aplicación puede personalizar y brindar una experiencia del usuario optimizada a los usuarios con limitaciones de costo y rendimiento.
Consulta Cómo ofrecer aplicaciones rápidas y livianas con Save-Data para obtener más información.
Carga inteligente basada en la información de la red
Te recomendamos que revises navigator.connection.type
antes de la carga previa. Cuando se establece en cellular
, puedes evitar la carga previa y advertir a los usuarios que su operador de red móvil podría cobrar por el ancho de banda, y solo comenzar la reproducción automática del contenido almacenado en caché anteriormente.
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 el ejemplo de información de red para aprender a reaccionar también a los cambios de red.
Cómo almacenar en caché previamente varios segmentos iniciales
Ahora bien, ¿qué sucede si quiero precargar de forma especulativa parte del contenido multimedia sin saber qué elemento multimedia elegirá el usuario? Si el usuario está en una página web que contiene 10 videos, es probable que tengamos suficiente memoria para recuperar un archivo de segmento de cada uno, pero definitivamente no debemos crear 10 elementos <video>
ocultos y 10 objetos MediaSource
y comenzar a alimentar esos datos.
En el siguiente ejemplo de dos partes, se muestra cómo almacenar en caché previamente varios primeros segmentos de video con la API de Cache, potente y fácil de usar. Ten en cuenta que también se puede lograr algo similar con IndexedDB. Aún no usamos service workers, ya que también se puede acceder a la API de caché desde el objeto window
.
Recuperación y almacenamiento en caché
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;
});
});
}
Ten en cuenta que, si usara solicitudes HTTP Range
, tendría que volver a crear manualmente
un objeto Response
, ya que la API de Cache aún no admite respuestas Range
. Ten en cuenta que llamar a networkResponse.arrayBuffer()
recupera todo el contenido de la respuesta a la vez en la memoria del renderizador, por lo que te recomendamos que uses rangos pequeños.
A modo de referencia, modifiqué parte del ejemplo anterior para guardar las solicitudes de rango HTTP en la caché previa 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;
});
Reproducir video
Cuando un usuario haga clic en un botón de reproducción, recuperaremos el primer segmento de video disponible en la API de Cache para que la reproducción comience de inmediato, si está disponible. De lo contrario, simplemente lo recuperaremos de la red. Ten en cuenta que los navegadores y los usuarios pueden decidir borrar la caché.
Como se vio antes, usamos MSE para enviar ese primer segmento de video al elemento de 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.
});
}
});
}
Crea respuestas de rango con un trabajador de servicio
Ahora bien, ¿qué sucede si recuperaste un archivo de video completo y lo guardaste en la API de Cache? Cuando el navegador envía una solicitud HTTP Range
, no querrás llevar todo el video a la memoria del renderizador, ya que la API de caché aún no admite respuestas Range
.
Te mostraré cómo interceptar estas solicitudes y mostrar una respuesta Range
personalizada desde 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;
}
}
Es importante tener en cuenta que usé response.blob()
para volver a crear esta respuesta cortada, ya que esto solo me brinda un control del archivo, mientras que response.arrayBuffer()
lleva todo el archivo a la memoria del renderizador.
Mi encabezado HTTP X-From-Cache
personalizado se puede usar para saber si esta solicitud provino de la caché o de la red. Un reproductor como ShakaPlayer puede usarlo para ignorar el tiempo de respuesta como indicador de la velocidad de la red.
Consulta la Sample Media App oficial y, en particular, su archivo ranged-response.js para obtener una solución completa sobre cómo controlar las solicitudes Range
.