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 assistindo seu vídeo ou ouvindo seus áudio. Fato conhecido. Neste artigo, explorarei técnicas que você pode usar para acelerar a reprodução de áudio e vídeo ao realizar ativamente pré-carregando recursos dependendo do caso de uso.
Vou descrever três métodos de pré-carregamento de 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 exclusivo hospedado em um servidor da Web. | Os navegadores podem ignorar completamente o atributo. |
A busca de recursos começa quando o documento HTML é totalmente carregado e analisados. | ||
As extensões de fonte de mídia (MSE, na sigla em inglês) ignoram o atributo preload nos 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 de um recurso de vídeo sem bloquear
evento onload do documento.
|
As solicitações de intervalo HTTP não são compatíveis. |
Compatível com MSE e segmentos de arquivo. | Use somente para arquivos de mídia pequenos (menos de 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, recomendamos
use o atributo de vídeo preload
para indicar ao navegador como
muitas informações ou conteúdos para pré-carregar. Isso significa que as extensões de fonte de mídia
(MSE) não é compatível com preload
.
A busca de recursos só será iniciada quando o documento HTML inicial for
completamente carregado e analisado (por exemplo, o evento DOMContentLoaded
foi disparado).
enquanto o evento load
muito diferente será disparado quando o recurso
foi realmente buscado.
Definir o atributo preload
como metadata
indica que o usuário não está
precisa do vídeo, mas buscar os metadados (dimensões,
lista, duração etc.) é desejável. A partir do Chrome
64, o valor padrão de preload
é metadata
. Era auto
antes).
<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
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 isso é apenas uma dica, o navegador pode
ignore o atributo preload
. No momento em que este artigo foi escrito, aqui estão algumas regras
aplicada 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 erro de Bug. - Em uma conexão celular (2G, 3G e 4G), o Chrome força o valor
preload
ametadata
.
Dicas
Caso seu site tenha muitos recursos de vídeo no mesmo domínio,
recomendamos que você defina o valor preload
como metadata
ou defina o poster
atributo e defina preload
como none
. Dessa forma, você evitaria bater
o número máximo de conexões HTTP para o mesmo domínio (seis de acordo com a
especificação HTTP 1.1), que podem travar o carregamento de recursos. Isso também pode
melhore a velocidade da página se os vídeos não fizerem parte da experiência principal do usuário.
Pré-carregamento de link
Como abordado em outros artigos, o pré-carregamento de link é uma busca declarativa que
permite que você force o navegador a solicitar um recurso sem
bloqueando o evento load
e durante o download da página. Recursos
carregados via <link rel="preload">
são armazenados localmente no navegador
ficam inertes até que sejam explicitamente referenciados no DOM, no JavaScript,
ou CSS.
O pré-carregamento é diferente da pré-busca porque se concentra na navegação e busca recursos com prioridade baseada no tipo (script, estilo, fonte, vídeo, áudio etc.). Ela deve ser usada para aquecer o cache do navegador para de conteúdo.
Pré-carregar vídeo completo
Veja como pré-carregar um vídeo completo em seu site para que, quando seu O JavaScript pede para buscar conteúdo de vídeo. Ele é lido do cache como o recurso já pode ter sido armazenado em cache pelo navegador. Se a solicitação de pré-carregamento for concluída, ocorrerá uma busca regular de rede.
<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
No exemplo, o valor do link de pré-carregamento as
é video
. Se fosse um áudio
elemento, 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 Media Source Extensions. Se você não conhece
com a API MSE JavaScript, consulte Noções básicas do MSE.
Para simplificar, vamos presumir que todo o vídeo 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
Você pode detectar o suporte a vários tipos de as
para <link rel=preload>
com o
snippets 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');
}
Armazenamento em buffer manual
Antes de nos aprofundarmos na API Cache e nos service workers, vamos
como armazenar vídeos em buffer manualmente com o EQM. O exemplo abaixo pressupõe que seu arquivo
servidor oferece suporte a HTTP Range
mas isso seria bem semelhante com arquivos
segmentos. Algumas bibliotecas de middleware, como a Shaka do Google
Player, JW Player e Video.js são
criado para lidar com 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 considerar o nível de bateria do dispositivo, o "modo de economia de dados" preferência do usuário e informações da rede ao pensar no pré-carregamento.
Reconhecimento de bateria
Considere o nível de bateria dos usuários dispositivos antes de pensar sobre o pré-carregamento de vídeos. Isso preservará a duração da bateria quando o nível de energia é baixo.
Desative o pré-carregamento ou pelo menos pré-carregue um vídeo de resolução mais baixa quando o o dispositivo está ficando sem bateria.
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 a "economia de dados"
Use o cabeçalho da solicitação de dica do cliente Save-Data
para fornecer dados rápidos e leves.
aplicativos a usuários que optaram pela "economia de dados" em suas
navegador. Ao identificar esse cabeçalho de solicitação, seu aplicativo pode personalizar
proporcionam uma experiência do usuário otimizada com limitações de custo e desempenho
usuários.
Consulte Como fornecer aplicativos rápidos e leves com Save-Data para saber mais.
Carregamento inteligente com base nas informações da rede
Verifique o navigator.connection.type
antes do pré-carregamento. Quando
ele estiver definido como cellular
, poderá impedir o pré-carregamento e informar aos usuários que
sua operadora de rede móvel esteja cobrando pela largura de banda e só comece a
reprodução automática de conteúdo armazenado em cache.
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 a amostra de informações de rede para saber como reagir à rede também muda.
Pré-armazenar em cache vários primeiros segmentos
E se eu quiser pré-carregar especulativamente algum conteúdo de mídia sem
sabendo qual parte da mídia o usuário escolherá? Se o usuário estiver em um
página com 10 vídeos, provavelmente temos memória suficiente para buscar um
arquivo de segmento de cada um, mas não devemos criar 10 arquivos <video>
ocultos
e 10 objetos MediaSource
e começar a alimentar esses dados.
O exemplo de duas partes abaixo mostra como armazenar previamente em cache vários primeiros segmentos de
usando a API Cache avançada e 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
.
Busca e 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
, precisaria recriar manualmente
um objeto Response
, porque a API Cache ainda não oferece suporte a respostas Range
. Tenha
lembrar que chamar networkResponse.arrayBuffer()
busca todo o conteúdo
da resposta de uma só vez na memória do renderizador. Por isso, é melhor usar
em pequenos intervalos.
Para referência, modifiquei parte do exemplo acima para salvar o intervalo HTTP para o 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 no botão de reprodução, buscamos o primeiro segmento do vídeo disponível na Cache API para que a reprodução inicie 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 vimos antes, usamos o EQM para alimentar o primeiro segmento do vídeo ao 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 Range com um service worker
Agora, e se você buscou um arquivo de vídeo inteiro e o salvou
a Cache API? Quando o navegador envia uma solicitação HTTP Range
, você não
quiser trazer todo o vídeo para a memória do renderizador, já que a API Cache não
oferecem suporte a respostas Range
ainda.
Vou mostrar como interceptar essas solicitações e retornar um Range
personalizado.
resposta 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 eu usei response.blob()
para recriar esse
porque isso simplesmente me dá um identificador para o arquivo enquanto
response.arrayBuffer()
leva 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
vieram do cache ou da rede. Ele pode ser usado por um jogador, como
ShakaPlayer para ignorar o tempo de resposta como um indicador de
a velocidade da rede.
Consulte o App de mídia de exemplo oficial, principalmente a biblioteca dele.
arquivo ranged-response.js para uma solução completa sobre como lidar com Range
solicitações.