Aprenda a usar o requestVideoFrameCallback()
para trabalhar de forma mais eficiente com vídeos no navegador.
O método HTMLVideoElement.requestVideoFrameCallback()
permite que os autores da Web registrem um callback
que é executado nas etapas de renderização quando um novo frame de vídeo é enviado ao compositor.
Isso permite que os desenvolvedores realizem operações eficientes por frame de vídeo em vídeo,
como processamento e pintura de vídeo em tela, análise de vídeo
ou sincronização com fontes de áudio externas.
Diferença com requestAnimationFrame()
Operações como desenhar um frame de vídeo em uma tela usando
drawImage()
feitas por essa API serão sincronizadas o máximo possível
com o frame rate do vídeo que está sendo reproduzido na tela.
Diferente de
window.requestAnimationFrame()
,
que normalmente é acionado cerca de 60 vezes por segundo,
o requestVideoFrameCallback()
está vinculado ao frame rate real do vídeo, com uma
exceção importante:
A taxa efetiva em que os callbacks são executados é a menor entre a taxa do vídeo e a do navegador. Isso significa que uma reprodução de vídeo de 25 fps em um navegador pintado a 60 Hz dispararia callbacks a 25 Hz. Um vídeo de 120 fps no mesmo navegador de 60 Hz dispararia callbacks a 60 Hz.
Do que é composto um nome?
Devido à semelhança com window.requestAnimationFrame()
, o método
inicialmente foi proposto como video.requestAnimationFrame()
e renomeado como
requestVideoFrameCallback()
, que foi acordado após uma longa discussão (links em inglês).
Detecção de recursos
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
Suporte ao navegador
Plástico poligonal
Um polyfill para o método requestVideoFrameCallback()
com base em Window.requestAnimationFrame()
e HTMLVideoElement.getVideoPlaybackQuality()
está disponível. Antes de usar, esteja ciente das
limitações mencionadas no README
.
Como usar o método requestVideoFrameCallback()
Se você já usou o método requestAnimationFrame()
, vai se familiarizar imediatamente com o método requestVideoFrameCallback()
.
Você registra um callback inicial uma vez e, em seguida, registra novamente sempre que o callback é acionado.
const doSomethingWithTheFrame = (now, metadata) => {
// Do something with the frame.
console.log(now, metadata);
// Re-register the callback to be notified about the next frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);
};
// Initially register the callback to be notified about the first frame.
video.requestVideoFrameCallback(doSomethingWithTheFrame);
No callback, now
é um DOMHighResTimeStamp
,
e metadata
é um dicionário VideoFrameMetadata
com as seguintes propriedades:
presentationTime
, do tipoDOMHighResTimeStamp
: o horário em que o user agent enviou o frame para composição.expectedDisplayTime
, do tipoDOMHighResTimeStamp
: o momento em que o user agent espera que o frame fique visível.width
, do tipounsigned long
: a largura do frame do vídeo, em pixels de mídia.height
, do tipounsigned long
: a altura do frame do vídeo, em pixels de mídia.mediaTime
, do tipodouble
: o carimbo de data/hora da apresentação de mídia (PTS, na sigla em inglês) em segundos do frame apresentado (por exemplo, o carimbo de data/hora na linha do tempovideo.currentTime
).presentedFrames
, do tipounsigned long
: uma contagem do número de frames enviados para composição. Permite que os clientes determinem se os frames foram perdidos entre as instâncias deVideoFrameRequestCallback
.processingDuration
, do tipodouble
: a duração em segundos desde o envio do pacote codificado com o mesmo carimbo de data/hora de apresentação (PTS, na sigla em inglês) desse frame (por exemplo, o mesmo que omediaTime
) ao decodificador, até que o frame decodificado esteja pronto para apresentação.
Para aplicativos WebRTC, propriedades adicionais podem aparecer:
captureTime
, do tipoDOMHighResTimeStamp
: para frames de vídeo provenientes de uma fonte local ou remota, é o momento em que o frame foi capturado pela câmera. No caso de uma fonte remota, o tempo de captura é estimado usando a sincronização de relógio e os relatórios do remetente RTCP para converter carimbos de data/hora RTP em hora de captura.receiveTime
, do tipoDOMHighResTimeStamp
: para frames de vídeo provenientes de uma fonte remota, esse é o horário em que o frame codificado foi recebido pela plataforma, ou seja, a hora em que o último pacote pertencente a esse frame foi recebido pela rede.rtpTimestamp
, do tipounsigned long
: o carimbo de data/hora do RTP associado a esse frame de vídeo.
De interesse especial nesta lista é mediaTime
.
A implementação do Chromium usa o relógio de áudio como a fonte de tempo que retorna video.currentTime
,
enquanto o mediaTime
é preenchido diretamente pelo presentationTimestamp
do frame.
O mediaTime
é o que você precisa usar se quiser identificar exatamente os frames de maneira reproduzível,
incluindo para identificar exatamente quais frames foram ignorados.
Se tudo parece estar um quadro fora do ar...
A sincronização vertical (ou apenas vsync) é uma tecnologia gráfica que sincroniza a taxa de quadros de um vídeo e a taxa de atualização de um monitor.
Como requestVideoFrameCallback()
é executada na linha de execução principal, mas, internamente, a composição de vídeos acontece na linha de execução do compositor.
Tudo o que essa API exige é o melhor esforço, e o navegador não oferece nenhuma garantia estrita.
O que pode estar acontecendo é que a API pode estar um vsync atrasado em relação ao momento em que um quadro de vídeo é renderizado.
É necessário um vsync para que as alterações feitas na página da Web por meio da API apareçam na tela (igual a window.requestAnimationFrame()
).
Se você continuar atualizando o mediaTime
ou o número do frame na sua página da Web e comparar isso
com os frames de vídeo numerados, o vídeo vai parecer que está um frame à frente.
O que está acontecendo é que o frame está pronto na vsync x, o callback é acionado e o frame é renderizado em vsync x+1,
e as alterações feitas no callback são renderizadas em vsync x+2.
É possível verificar se o callback é uma vsync atrasada (e o frame já está renderizado na tela)
verificando se o metadata.expectedDisplayTime
é aproximadamente now
ou um vsync no futuro.
Se estiver dentro de cinco a dez microssegundos de now
, o frame já terá sido renderizado.
Se o expectedDisplayTime
estiver com aproximadamente 16 milissegundos no futuro (supondo que o navegador/tela esteja sendo atualizado a 60 Hz),
o frame estará sincronizado.
Demonstração
Criei uma pequena demonstração do Glitch que mostra como os frames são desenhados em uma tela exatamente no frame rate do vídeo e onde os metadados do frame são registrados para fins de depuração.
let paintCount = 0;
let startTime = 0.0;
const updateCanvas = (now, metadata) => {
if (startTime === 0.0) {
startTime = now;
}
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const elapsed = (now - startTime) / 1000.0;
const fps = (++paintCount / elapsed).toFixed(3);
fpsInfo.innerText = `video fps: ${fps}`;
metadataInfo.innerText = JSON.stringify(metadata, null, 2);
video.requestVideoFrameCallback(updateCanvas);
};
video.requestVideoFrameCallback(updateCanvas);
Conclusões
As pessoas fizeram processamento no nível do frame por muito tempo, sem ter acesso aos frames reais,
apenas com base em video.currentTime
.
O método requestVideoFrameCallback()
melhora muito essa solução alternativa.
Agradecimentos
A API requestVideoFrameCallback
foi especificada e implementada por Thomas Guilbert (link em inglês).
Esta postagem foi revisada por Joe Medley
e Kayce Basques.
Imagem principal por
Denise Jans no Unsplash.