Como acelerar a reprodução de mídia pré-carregando ativamente recursos.
Um início de reprodução mais rápido significa que mais pessoas vão assistir seu vídeo ou ouvir seu áudio. Isso é um fato conhecido. Neste artigo, veremos técnicas que podem ser usadas para acelerar a reprodução de áudio e vídeo, pré-carregando ativamente os recursos, dependendo do caso de uso.
Vou descrever três métodos de pré-carregamento de arquivos de mídia, começando com seus prós e contras.
É ótimo... | Mas… | |
---|---|---|
Atributo de pré-carregamento de vídeo | Simples de usar para um arquivo único hospedado em um servidor da Web. | Os navegadores podem ignorar completamente o atributo. |
A busca de recursos começa quando o documento HTML é completamente carregado e analisado. | ||
As extensões de fonte de mídia (MSE, na sigla em inglês) ignoram o atributo preload em elementos de mídia porque o app é responsável por
fornecer mídia ao EQM.
|
||
Pré-carregamento de link |
Força o navegador a fazer uma solicitação para um recurso de vídeo sem bloquear
o evento onload do documento.
|
As solicitações de intervalo HTTP não são compatíveis. |
Compatível com MSE e segmentos de arquivo. | Deve ser usado apenas para arquivos de mídia pequenos (<5 MB) ao buscar recursos completos. | |
Armazenamento em buffer manual | Controle total | O tratamento complexo de erros é responsabilidade do site. |
Atributo de pré-carregamento de vídeo
Se a origem do vídeo for um arquivo exclusivo hospedado em um servidor da Web, use o atributo preload
do vídeo para indicar ao navegador quantas
informações ou conteúdos serão pré-carregados. Isso significa que as Extensões de origem de mídia
(MSE) não são compatíveis com preload
.
A busca de recursos será iniciada somente quando o documento HTML inicial for
completamente carregado e analisado (por exemplo, o evento DOMContentLoaded
ser disparado),
enquanto o evento load
, que é bem diferente, será disparado quando o recurso
for realmente buscado.
A configuração do atributo preload
como metadata
indica que o usuário não
precisa do vídeo, mas que é desejável buscar os metadados dele (dimensões, lista
de faixas, duração etc.). A partir do Chrome
64, o valor padrão de preload
é 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>
Definir o atributo preload
como auto
indica que o navegador pode armazenar em cache
dados suficientes para que a reprodução completa seja possível sem precisar parar para
mais armazenamento em buffer.
<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>
No entanto, há algumas ressalvas. Como essa é apenas uma dica, o navegador pode ignorar completamente
o atributo preload
. No momento em que este artigo foi escrito, aqui estão algumas regras
aplicadas no Chrome:
- Quando a Economia de dados está ativada, o Chrome força o valor
preload
paranone
. - No Android 4.3, o Chrome força o valor
preload
paranone
devido a um bug do Android. - Em uma conexão de celular (2G, 3G e 4G), o Chrome força o valor
preload
parametadata
.
Dicas
Se o site tiver muitos recursos de vídeo no mesmo domínio, recomendamos
definir o valor preload
como metadata
ou o atributo poster
e definir preload
como none
. Dessa forma, você evita atingir
o número máximo de conexões HTTP para o mesmo domínio (6 de acordo com a
especificação HTTP 1.1), o que pode travar o carregamento de recursos. Isso também pode
melhorar a velocidade da página se os vídeos não fizerem parte da sua experiência principal do usuário.
Pré-carregar link
Como explicado em outros artigos, o pré-carregamento de link é um fetch declarativo que
permite forçar o navegador a fazer uma solicitação de um recurso sem
bloquear o evento load
e enquanto a página está sendo transferida. Os recursos carregados por <link rel="preload">
são armazenados localmente no navegador e ficam efetivamente inertes até serem explicitamente referenciados no DOM, JavaScript ou CSS.
O pré-carregamento é diferente da pré-busca porque se concentra na navegação atual e busca recursos com prioridade com base no tipo (script, estilo, fonte, vídeo, áudio etc.). Ele deve ser usado para aquecer o cache do navegador para as sessões atual.
Pré-carregar vídeo completo
Confira como pré-carregar um vídeo completo no seu site para que, quando o JavaScript solicitar a busca de conteúdo de vídeo, ele seja lido do cache, porque o recurso pode já ter sido armazenado em cache pelo navegador. Se a solicitação de pré-carregamento ainda não tiver sido concluída, uma busca de rede regular vai acontecer.
<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 o recurso pré-carregado será consumido por um elemento de vídeo no
exemplo, o valor do link de pré-carregamento as
é video
. Se fosse um elemento
de áudio, seria as="audio"
.
Pré-carregue o primeiro segmento
O exemplo abaixo mostra como pré-carregar o primeiro segmento de um vídeo com <link
rel="preload">
e usá-lo com extensões de origem de mídia. Se você não estiver familiarizado
com a API JavaScript MSE, consulte Noções básicas de MSE.
Para simplificar, vamos supor que o vídeo inteiro foi dividido em
arquivos menores, como file_1.webm
, file_2.webm
, file_3.webm
etc.
<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>
Suporte
É possível detectar o suporte a vários tipos de as
para <link rel=preload>
com os
trechos abaixo:
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');
}
Bufferização manual
Antes de nos aprofundarmos na API Cache e nos service workers, vamos ver
como armazenar um vídeo em buffer manualmente com o MSE. O exemplo abaixo pressupõe que seu servidor da Web
oferece suporte a solicitações HTTP Range
,
mas isso seria muito semelhante com segmentos de
arquivos. Algumas bibliotecas de middleware, como Shaka
Player do Google, JW Player e Video.js (links em inglês), são
criadas para processar isso para você.
<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>
Considerações
Como agora você controla toda a experiência de armazenamento em buffer de mídia, sugiro que você considere o nível de bateria do dispositivo, a preferência do usuário "Modo de economia de dados" e as informações da rede ao pensar no pré-carregamento.
Uso da bateria
Considere o nível de bateria dos dispositivos dos usuários antes de pensar em pré-carregar um vídeo. Isso preservará a duração da bateria quando o nível da bateria estiver baixo.
Desative o pré-carregamento ou, pelo menos, pré-carregue um vídeo de resolução mais baixa quando a bateria do dispositivo estiver acabando.
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.
}
});
}
Detectar "Economia de dados"
Use o cabeçalho de solicitação de dica do cliente Save-Data
para oferecer apps rápidos e leves
aos usuários que ativaram o modo "Economia de dados" no
navegador. Ao identificar esse cabeçalho de solicitação, seu aplicativo pode personalizar e
oferecer uma experiência do usuário otimizada a usuários com restrições
de custo e desempenho.
Consulte Como fornecer aplicativos rápidos e leves com Save-Data para saber mais.
Carregamento inteligente com base nas informações da rede
Verifique navigator.connection.type
antes do carregamento. Quando
ela estiver definida como cellular
, será possível impedir o pré-carregamento e informar aos usuários que
a operadora de rede móvel pode estar cobrando pela largura de banda e apenas iniciar
a reprodução automática do conteúdo armazenado em cache 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.
}
}
Confira o exemplo de informações de rede para saber como reagir às mudanças de rede.
Pré-armazenar em cache vários primeiros segmentos
E se eu quiser pré-carregar especulativamente algum conteúdo de mídia sem
saber qual mídia o usuário escolherá? Se o usuário estiver em uma
página da Web que contém 10 vídeos, provavelmente teremos memória suficiente para buscar um
arquivo de segmento de cada um, mas não devemos criar 10 elementos <video>
ocultos e 10 objetos MediaSource
e começar a alimentar esses dados.
O exemplo de duas partes abaixo mostra como pré-cachear vários primeiros segmentos de
vídeo usando a API Cache, que é fácil de usar. Algo semelhante também pode ser alcançado com o IndexedDB. Ainda não estamos usando service workers, porque
a API Cache também pode ser acessada pelo objeto window
.
Buscar e armazenar em 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;
});
});
}
Se eu usasse solicitações HTTP Range
, teria que recriar manualmente
um objeto Response
, já que a API Cache ainda não oferece suporte a respostas Range
. Lembre-se de que chamar networkResponse.arrayBuffer()
busca todo o conteúdo da resposta de uma só vez na memória do renderizador. É por isso que você pode querer usar intervalos pequenos.
Como referência, modifiquei parte do exemplo acima para salvar solicitações de intervalo HTTP no pré-cache de vídeo.
...
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;
});
Iniciar vídeo
Quando um usuário clica em um botão de reprodução, buscamos o primeiro segmento de vídeo disponível na API Cache para que a reprodução comece imediatamente, se disponível. Caso contrário, vamos simplesmente buscá-lo na rede. Lembre-se de que os navegadores e os usuários podem decidir limpar o cache.
Como visto anteriormente, usamos o MSE para alimentar o primeiro segmento de vídeo para o elemento de vídeo.
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.
});
}
});
}
Criar respostas de intervalo com um worker de serviço
E se você tiver buscado um arquivo de vídeo inteiro e salvo na
API Cache? Quando o navegador envia uma solicitação HTTP Range
, não convém colocar o vídeo inteiro na memória do renderizador, já que a API Cache ainda não é compatível com respostas Range
.
Vou mostrar como interceptar essas solicitações e retornar uma resposta Range
personalizada de um 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 observar que usei response.blob()
para recriar essa resposta
dividida, já que isso apenas fornece um identificador para o arquivo, enquanto
response.arrayBuffer()
traz o arquivo inteiro para a memória do renderizador.
Meu cabeçalho HTTP X-From-Cache
personalizado pode ser usado para saber se essa solicitação
veio do cache ou da rede. Ela pode ser usada por um jogador como o
ShakaPlayer para ignorar o tempo de resposta como um indicador da
velocidade da rede.
Confira o Sample Media App oficial e, em particular, o arquivo
ranged-response.js para uma solução completa sobre como processar solicitações
Range
.