Realizar operações eficientes por frame de vídeo em vídeo com requestVideoFrameCallback()

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 ao compositor. Isso permite que os desenvolvedores realizem operações eficientes por frame de vídeo, como processamento e pintura em uma tela, análise 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 exibido na tela. Ao contrário de window.requestAnimationFrame(), que geralmente é disparado cerca de 60 vezes por segundo, requestVideoFrameCallback() está vinculado à taxa de frames 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 um vídeo de 25 QPS reproduzido em um navegador que renderiza a 60 Hz acionaria callbacks a 25 Hz. Um vídeo de 120 QPS 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 foi inicialmente 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

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: 132.
  • Safari: 15.4.

Source

Polyfill

Um polyfill para o método requestVideoFrameCallback() baseado em Window.requestAnimationFrame() e HTMLVideoElement.getVideoPlaybackQuality() está disponível. Antes de usar esse recurso, conheça as limitações mencionadas em 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 depois o registra novamente sempre que ele é 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 tipo DOMHighResTimeStamp: o momento em que o user agent enviou o frame para composição.
  • expectedDisplayTime, do tipo DOMHighResTimeStamp: o momento em que o user agent espera que o frame fique visível.
  • width, do tipo unsigned long: a largura do frame de vídeo, em pixels de mídia.
  • height, do tipo unsigned long: a altura do frame de vídeo, em pixels de mídia.
  • mediaTime, do tipo double: o carimbo de data/hora de 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 tempo video.currentTime).
  • presentedFrames, do tipo unsigned long: uma contagem do número de frames enviados para composição. Permite que os clientes determinem se há falhas de frames entre instâncias de VideoFrameRequestCallback.
  • processingDuration, do tipo double: a duração decorrida em segundos desde o envio do pacote codificado com o mesmo carimbo de data/hora de apresentação (PTS) deste frame (por exemplo, igual ao mediaTime) até o decodificador, até que o frame decodificado estivesse pronto para apresentação.

Para aplicativos WebRTC, outras propriedades podem aparecer:

  • captureTime, do tipo DOMHighResTimeStamp: Para frames de vídeo de uma fonte local ou remota, esse é o momento em que o frame foi capturado pela câmera. Para 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 tempo de captura.
  • receiveTime, do tipo DOMHighResTimeStamp: para frames de vídeo de uma fonte remota, esse é o momento em que o frame codificado foi recebido pela plataforma, ou seja, o momento em que o último pacote pertencente a esse frame foi recebido pela rede.
  • rtpTimestamp, do tipo unsigned long: O carimbo de data/hora do RTP associado a este frame de vídeo.

De especial interesse nesta lista está mediaTime. A implementação do Chromium usa o relógio de áudio como a fonte de tempo que respalda video.currentTime, enquanto o mediaTime é preenchido diretamente pelo presentationTimestamp do frame. O mediaTime é o que você deve usar se quiser identificar frames de maneira reproduzível, incluindo para identificar exatamente quais frames você perdeu.

Se as coisas parecerem um frame fora de sincronia…

A sincronização vertical (ou apenas vsync) é uma tecnologia gráfica que sincroniza o frame rate de um vídeo e a taxa de atualização de um monitor. Como requestVideoFrameCallback() é executado na linha de execução principal, mas, por baixo dos panos, 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 um vsync atrasada em relação ao momento em que um frame de vídeo é renderizado. É necessário um vsync para que as mudanças feitas na página da Web pela API apareçam na tela (o mesmo que window.requestAnimationFrame()). Portanto, se você continuar atualizando o mediaTime ou o número do frame na página da Web e comparar isso com os frames numerados do vídeo, eventualmente o vídeo vai parecer estar um frame à frente.

O que realmente acontece é que o frame fica pronto no vsync x, o callback é acionado e o frame é renderizado no vsync x+1, e as mudanças feitas no callback são renderizadas no vsync x+2. Para verificar se o callback está atrasado em relação à vsync (e se o frame já foi renderizado na tela), confira se o metadata.expectedDisplayTime é aproximadamente now ou uma vsync no futuro. Se estiver dentro de cerca de cinco a dez microssegundos de now, o frame já será renderizado. Se o expectedDisplayTime estiver aproximadamente 16 milissegundos no futuro (supondo que seu navegador/tela esteja sendo atualizado a 60 Hz), você estará sincronizado com o frame.

Demonstração

Criei uma pequena demonstração (link em inglês) que mostra como os frames são desenhados em uma tela na taxa de frames exata do vídeo e onde os metadados de 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 o processamento no nível do 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.