Como acelerar a reprodução de mídia fazendo o pré-carregamento ativo de recursos.
O início mais rápido da reprodução significa que mais pessoas estão assistindo seu vídeo ou ouvindo seu áudio. Isso é um fato conhecido. Neste artigo, vamos explorar técnicas que você pode usar para acelerar a reprodução de áudio e vídeo com o pré-carregamento ativo de recursos, dependendo do seu caso de uso.
Vou descrever três métodos de pré-carregar arquivos de mídia, começando pelos 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 mídia (MSE) ignoram o atributo preload em elementos de mídia porque o app é responsável por
fornecer mídia para a MSE.
|
||
Pré-carregamento de links |
Força o navegador a fazer uma solicitação de 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 processamento de erros complexos é responsabilidade do site. |
Atributo de pré-carregamento de vídeo
Se a origem do vídeo for um arquivo único hospedado em um servidor da Web, use o atributo preload
para indicar ao navegador a quantidade de
informações ou conteúdo a ser pré-carregado. Isso significa que as Extensões de origem de mídia
(MSE) não são compatíveis com preload
.
A busca de recursos só vai começar quando o documento HTML inicial for
completamente carregado e analisado (por exemplo, o evento DOMContentLoaded
foi acionado),
enquanto o evento load
muito diferente será acionado 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>
A definição do 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 bufferização.
<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 da redação deste artigo, estas são algumas regras
aplicadas no Chrome:
- Quando a Economia de dados está ativada, o Chrome força o valor
preload
anone
. - 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 seu site tiver muitos recursos de vídeo no mesmo domínio, recomendo definir o valor de preload
como metadata
ou definir 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 links é 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 são
efetivamente inativos até serem referenciados explicitamente no DOM, JavaScript
ou CSS.
O pré-carregamento é diferente do pré-carregamento, porque se concentra na navegação atual e busca recursos com prioridade com base no tipo deles (script, estilo, fonte, vídeo, áudio etc.). Ele deve ser usado para aquecer o cache do navegador para as sessões atuais.
Pré-carregar o vídeo completo
Veja como pré-carregar um vídeo completo no seu site para que, quando o JavaScript pedir para buscar o conteúdo do vídeo, ele seja lido do cache, já que 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é-carregar 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 conhece
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 mergulharmos na API Cache e nos service workers, vamos conferir
como armazenar um vídeo manualmente com MSE. O exemplo abaixo pressupõe que seu servidor
da Web ofereça suporte a solicitações HTTP Range
,
mas isso seria muito semelhante aos segmentos
de arquivo. Algumas bibliotecas de middleware, como o Shaka Player
do Google, o JW Player e o Video.js, foram
criadas para lidar com isso.
<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 você agora tem controle de toda a experiência de bufferização de mídia, sugerimos 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 de rede ao pensar sobre o pré-carregamento.
Uso da bateria
Leve em conta o nível da bateria dos dispositivos dos usuários antes de pensar em pré-carregar um vídeo. Isso preserva a duração da bateria quando o nível de energia está 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 sugestão de cliente Save-Data
para oferecer aplicativos rápidos e leves
a usuários que ativaram o modo de "economia de dados" no
navegador. Ao identificar esse cabeçalho de solicitação, o aplicativo pode personalizar e
oferecer uma experiência otimizada para usuários com restrições de custo e
desempenho.
Consulte Como oferecer aplicativos rápidos e leves com o Save-Data para saber mais.
Carregamento inteligente com base em informações de rede
Verifique navigator.connection.type
antes do carregamento. Quando
ele está definido como cellular
, é possível impedir o pré-carregamento e avisar os usuários de que
o operador de rede móvel pode estar cobrando pela largura de banda e iniciar apenas
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é-cachear vários primeiros segmentos
E se eu quiser pré-carregar algum conteúdo de mídia sem
saber qual o usuário vai 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
. Chamar
networkResponse.arrayBuffer()
busca todo o conteúdo
da resposta de uma só vez na memória do renderizador. Por isso, use
intervalos pequenos.
Para referência, modifiquei parte do exemplo acima para salvar solicitações de intervalo HTTP no pré-cache do 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 buscar na rede. 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 o salvo na
API Cache? Quando o navegador envia uma solicitação HTTP Range
, você não
quer colocar o vídeo inteiro na memória do renderizador, porque a API Cache ainda não
oferece suporte a respostas Range
.
Vou mostrar como interceptar essas solicitações e retornar uma resposta Range
personalizada de um worker de serviço.
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
fragmentada, porque isso simplesmente me dá um identificador para o arquivo, enquanto
response.arrayBuffer()
traz todo o arquivo 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. Ele pode ser usado por um player, como
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
.