Saiba como usar o requestVideoFrameCallback()
para trabalhar com mais eficiência com vídeos no navegador.
O método HTMLVideoElement.requestVideoFrameCallback()
permite que autores da Web registrem um callback
que é executado nas etapas de renderização quando um novo frame de vídeo é enviado para o compositor.
Isso permite que os desenvolvedores realizem operações eficientes por frame de vídeo,
como processamento e pintura em uma 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 da melhor forma
possível com a taxa de frames do vídeo reproduzido na tela.
Diferente de
window.requestAnimationFrame()
,
que geralmente é acionado cerca de 60 vezes por segundo,
requestVideoFrameCallback()
é vinculado à taxa de quadros real do vídeo, com uma importante
exceção:
A taxa efetiva em que os callbacks são executados é a menor taxa entre a taxa do vídeo e a taxa do navegador. Isso significa que um vídeo de 25 fps reproduzido em um navegador que pinta a 60 Hz dispararia callbacks a 25 Hz. Um vídeo de 120 fps nesse mesmo navegador de 60 Hz aciona 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()
, o que foi acordado
após uma longa discussão.
Detecção de recursos
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
// The API is supported!
}
Suporte ao navegador
Polyfill
Um polyfill para o método requestVideoFrameCallback()
com base em
Window.requestAnimationFrame()
e
HTMLVideoElement.getVideoPlaybackQuality()
está disponível. Antes de usar, conheça as
limitações mencionadas no README
.
Como usar o método requestVideoFrameCallback()
Se você já usou o método requestAnimationFrame()
, vai se sentir familiarizado com o método requestVideoFrameCallback()
.
Você registra um callback inicial uma vez e depois o 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 momento em que o user agent enviou o frame para composição.expectedDisplayTime
, do tipoDOMHighResTimeStamp
: o tempo em que o agente do usuário espera que o frame esteja 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, 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 frames foram perdidos entre instâncias deVideoFrameRequestCallback
.processingDuration
, do tipodouble
: a duração em segundos desde o envio do pacote codificado com o mesmo carimbo de tempo de apresentação (PTS, na sigla em inglês) que este frame (por exemplo, o mesmo que omediaTime
) para o decodificador até que o frame decodificado estivesse pronto para apresentação.
Em aplicativos WebRTC, outras propriedades podem ser exibidas:
captureTime
, do tipoDOMHighResTimeStamp
: para frames de vídeo provenientes de uma origem local ou remota, esse é o momento em que o frame foi capturado pela câmera. Para uma fonte remota, o tempo de captura é estimado com o uso de sincronização de relógio e relatórios do remetente do RTCP para converter carimbos de data/hora do RTP em tempo 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, o horário em que o último pacote pertencente a esse frame foi recebido pela rede.rtpTimestamp
, do tipounsigned long
: o carimbo de data/hora RTP associado a esse frame de vídeo.
O item de interesse especial nesta lista é mediaTime
.
A implementação do Chromium usa o relógio de áudio como a fonte de horário que apoia 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 uma maneira reproduzível,
inclusive para identificar exatamente quais frames você perdeu.
Se as coisas parecerem um frame fora…
A sincronização vertical (ou apenas vsync) é uma tecnologia gráfica que sincroniza a taxa de frames de um vídeo e a taxa de atualização de um monitor.
Como requestVideoFrameCallback()
é executado na linha de execução principal, mas, por trás, a composição de vídeo acontece na linha de execução do compositor,
tudo dessa API é um esforço máximo, e o navegador não oferece garantias estritas.
O que pode estar acontecendo é que a API pode estar com atraso de uma vsync em relação ao momento em que um frame de vídeo é renderizado.
É necessária uma vsync para que as mudanças feitas na página da Web pela API apareçam na tela (igual a window.requestAnimationFrame()
).
Assim, 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, eventualmente o vídeo terá a aparência de um frame à frente.
O que realmente acontece é que o frame fica pronto em vsync x, o callback é acionado e o frame é renderizado em vsync x+1,
e as mudanças feitas no callback são renderizadas em vsync x+2.
É possível verificar se o callback é um vsync atrasado (e o frame já foi renderizado na tela)
verificando se o metadata.expectedDisplayTime
é aproximadamente now
ou um vsync no futuro.
Se estiver em cerca de 5 a 10 microssegundos de now
, o frame já estará renderizado.
Se o expectedDisplayTime
estiver aproximadamente 16 milissegundos no futuro (supondo que seu navegador/tela esteja atualizando a 60 Hz),
você está sincronizado com o frame.
Demonstração
Criei uma pequena demonstração no Glitch que mostra como os frames são renderizados em uma tela exatamente na taxa de frames 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 fazem processamento em nível de frame há 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.
Esta postagem foi revisada por Joe Medley
e Kayce Basques.
Imagem principal de
Denise Jans no Unsplash.