Processamento de vídeo com WebCodecs

Manipular componentes de stream de vídeo.

Eugene Zemtsov
Eugene Zemtsov
François Beaufort
François Beaufort

As tecnologias modernas da web oferecem diversas maneiras de trabalhar com vídeo. API Media Stream, API Media Recording, API Media Source, e a API WebRTC com um conjunto avançado de ferramentas para gravar, transferir e reproduzir streams de vídeo. Ao resolver certas tarefas de alto nível, essas APIs não permitem os programadores trabalham com componentes individuais de um stream de vídeo, como quadros e não multiplexados de vídeo ou áudio codificado. Para ter acesso de baixo nível a esses componentes básicos, os desenvolvedores têm usado WebAssembly para trazer codecs de vídeo e áudio para o navegador. Mas, considerando que os navegadores modernos já vêm com uma variedade de codecs (que são frequentemente acelerado por hardware), repaginando-os, já que o WebAssembly parece um desperdício humanos e computacionais.

A API WebCodecs elimina essa ineficiência oferecendo aos programadores uma maneira de usar componentes de mídia que já estão presentes no navegador. Especificamente:

  • Decodificadores de vídeo e áudio
  • Codificadores de vídeo e áudio
  • Frames de vídeo brutos
  • Decodificadores de imagem

A API WebCodecs é útil para aplicativos da web que exigem controle total sobre a maneira como o conteúdo de mídia é processado, como editores de vídeo, videoconferências, streaming etc.

Fluxo de trabalho de processamento de vídeo

Os frames são o item central do processamento de vídeo. Assim, no WebCodecs, a maioria das classes consumir ou produzir frames. Codificadores de vídeo convertem frames em pedaços Os decodificadores de vídeo fazem o contrário.

Além disso, VideoFrame funciona bem com outras APIs da Web porque é um CanvasImageSource e tem um construtor que aceita CanvasImageSource. Por isso, ela pode ser usada em funções como drawImage() e texImage2D(). Além disso, ele pode ser construído a partir de telas, bitmaps, elementos de vídeo e outros frames de vídeo.

A API WebCodecs funciona bem em conjunto com as classes da API Insertable Streams. que conectam o WebCodecs a faixas de stream de mídia.

  • O MediaStreamTrackProcessor divide as faixas de mídia em frames individuais.
  • O MediaStreamTrackGenerator cria uma faixa de mídia com base em um stream de frames.

WebCodecs e web workers

Por design, a API WebCodecs faz todo o trabalho pesado de forma assíncrona e fora da linha de execução principal. Mas, como retornos de chamada de frame e bloco podem ser chamados diversas vezes por segundo, elas podem sobrecarregar a linha de execução principal e tornar o site menos responsivo. Portanto, é preferível mover o tratamento de frames individuais e blocos codificados para um worker da Web.

Para ajudar com isso, ReadableStream oferece uma maneira conveniente de transferir automaticamente todos os frames provenientes de uma mídia para o worker. Por exemplo, MediaStreamTrackProcessor pode ser usado para conseguir uma ReadableStream para uma faixa de stream de mídia proveniente da webcam. Depois disso o stream é transferido para um web worker, em que os frames são lidos um por um e colocados na fila. em um VideoEncoder.

Com a HTMLCanvasElement.transferControlToOffscreen, até mesmo a renderização pode ser feita fora da linha de execução principal. Mas, se todas as ferramentas de alto nível inconveniente, o próprio VideoFrame é transferível e pode ser movidos entre os workers.

WebCodecs em ação

Codificação

O caminho de um Canvas ou ImageBitmap para a rede ou para o armazenamento
O caminho de um Canvas ou ImageBitmap para a rede ou para o armazenamento

Tudo começa com uma VideoFrame. Há três maneiras de criar frames de vídeo.

  • De uma origem de imagem, como uma tela, um bitmap de imagem ou um elemento de vídeo.

    const canvas = document.createElement("canvas");
    // Draw something on the canvas...
    
    const frameFromCanvas = new VideoFrame(canvas, { timestamp: 0 });
    
  • Usar MediaStreamTrackProcessor para extrair frames de uma MediaStreamTrack

    const stream = await navigator.mediaDevices.getUserMedia({});
    const track = stream.getTracks()[0];
    
    const trackProcessor = new MediaStreamTrackProcessor(track);
    
    const reader = trackProcessor.readable.getReader();
    while (true) {
      const result = await reader.read();
      if (result.done) break;
      const frameFromCamera = result.value;
    }
    
  • Criar um frame a partir da representação de pixels binários em um BufferSource

    const pixelSize = 4;
    const init = {
      timestamp: 0,
      codedWidth: 320,
      codedHeight: 200,
      format: "RGBA",
    };
    const data = new Uint8Array(init.codedWidth * init.codedHeight * pixelSize);
    for (let x = 0; x < init.codedWidth; x++) {
      for (let y = 0; y < init.codedHeight; y++) {
        const offset = (y * init.codedWidth + x) * pixelSize;
        data[offset] = 0x7f;      // Red
        data[offset + 1] = 0xff;  // Green
        data[offset + 2] = 0xd4;  // Blue
        data[offset + 3] = 0x0ff; // Alpha
      }
    }
    const frame = new VideoFrame(data, init);
    

Não importa de onde vêm, os frames podem ser codificados Objetos EncodedVideoChunk com um VideoEncoder.

Antes da codificação, VideoEncoder precisa receber dois objetos JavaScript:

  • Inicializar o dicionário com duas funções para lidar com blocos codificados e erros. Essas funções são definidas pelo desenvolvedor e não podem ser alteradas depois elas são transmitidas para o construtor VideoEncoder.
  • Objeto de configuração do codificador, que contém parâmetros para a saída stream de vídeo. É possível mudar esses parâmetros mais tarde chamando configure().

O método configure() vai gerar NotSupportedError se a configuração não for compatíveis com o navegador. Recomendamos que você chame o método estático VideoEncoder.isConfigSupported() pela configuração para verificar antecipadamente se até que a configuração seja aceita e aguarde a promessa.

const init = {
  output: handleChunk,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  width: 640,
  height: 480,
  bitrate: 2_000_000, // 2 Mbps
  framerate: 30,
};

const { supported } = await VideoEncoder.isConfigSupported(config);
if (supported) {
  const encoder = new VideoEncoder(init);
  encoder.configure(config);
} else {
  // Try another config.
}

Depois de configurar o codificador, ele estará pronto para aceitar frames pelo método encode(). Tanto configure() quanto encode() retornam imediatamente sem esperar pela que o trabalho real seja concluído. Ela permite que vários frames sejam enfileirados para codificação no ao mesmo tempo, enquanto encodeQueueSize mostra quantas solicitações estão aguardando na fila para que as codificações anteriores sejam concluídas. Os erros são relatados com o lançamento imediato de uma exceção, caso os argumentos ou se a ordem das chamadas de método viola o contrato da API ou chama o método error() para problemas encontrados na implementação do codec. Se a codificação for concluída com êxito, o output() é chamado com um novo bloco codificado como argumento. Outro detalhe importante aqui é que os frames precisam ser informados não precisa mais chamando close().

let frameCounter = 0;

const track = stream.getVideoTracks()[0];
const trackProcessor = new MediaStreamTrackProcessor(track);

const reader = trackProcessor.readable.getReader();
while (true) {
  const result = await reader.read();
  if (result.done) break;

  const frame = result.value;
  if (encoder.encodeQueueSize > 2) {
    // Too many frames in flight, encoder is overwhelmed
    // let's drop this frame.
    frame.close();
  } else {
    frameCounter++;
    const keyFrame = frameCounter % 150 == 0;
    encoder.encode(frame, { keyFrame });
    frame.close();
  }
}

Por fim, é hora de terminar a codificação do código escrevendo uma função que lida com pedaços de vídeo codificado à medida que saem do codificador. Em geral, essa função seria enviar blocos de dados pela rede ou muxinhá-los em um objeto contêiner para armazenamento.

function handleChunk(chunk, metadata) {
  if (metadata.decoderConfig) {
    // Decoder needs to be configured (or reconfigured) with new parameters
    // when metadata has a new decoderConfig.
    // Usually it happens in the beginning or when the encoder has a new
    // codec specific binary configuration. (VideoDecoderConfig.description).
    fetch("/upload_extra_data", {
      method: "POST",
      headers: { "Content-Type": "application/octet-stream" },
      body: metadata.decoderConfig.description,
    });
  }

  // actual bytes of encoded data
  const chunkData = new Uint8Array(chunk.byteLength);
  chunk.copyTo(chunkData);

  fetch(`/upload_chunk?timestamp=${chunk.timestamp}&type=${chunk.type}`, {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: chunkData,
  });
}

Se em algum momento você precisar garantir que todas as solicitações de codificação pendentes tenham for concluída, chame flush() e aguarde a promessa.

await encoder.flush();

Decodificação

O caminho da rede ou do armazenamento para um Canvas ou ImageBitmap.
O caminho da rede ou do armazenamento para um Canvas ou um ImageBitmap.

A configuração de um VideoDecoder é semelhante ao que foi feito para o VideoEncoder: duas funções são transmitidas quando o decodificador é criado, e o codec parâmetros são fornecidos a configure().

O conjunto de parâmetros do codec varia de acordo com o codec. Por exemplo, o codec H.264 pode precisar de um blob binário do AVCC, a menos que esteja codificado no formato do Anexo B (encoderConfig.avc = { format: "annexb" }).

const init = {
  output: handleFrame,
  error: (e) => {
    console.log(e.message);
  },
};

const config = {
  codec: "vp8",
  codedWidth: 640,
  codedHeight: 480,
};

const { supported } = await VideoDecoder.isConfigSupported(config);
if (supported) {
  const decoder = new VideoDecoder(init);
  decoder.configure(config);
} else {
  // Try another config.
}

Depois que o decodificador for inicializado, você poderá começar a alimentá-lo com objetos EncodedVideoChunk. Para criar um bloco, você precisará de:

  • Um BufferSource de dados de vídeo codificados
  • carimbo de data/hora de início do bloco em microssegundos (tempo de mídia do primeiro frame codificado no bloco)
  • o tipo de bloco, um de:
    • key se o bloco puder ser decodificado de forma independente de blocos anteriores
    • delta se o bloco só puder ser decodificado depois que um ou mais blocos anteriores forem decodificados

Além disso, todos os blocos emitidos pelo codificador estão prontos para o decodificador no estado em que se encontram. Tudo o que foi dito acima sobre relatórios de erros e a natureza assíncrona dos métodos do codificador também são verdadeiros para os decodificadores.

const responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
  const chunk = new EncodedVideoChunk({
    timestamp: responses[i].timestamp,
    type: responses[i].key ? "key" : "delta",
    data: new Uint8Array(responses[i].body),
  });
  decoder.decode(chunk);
}
await decoder.flush();

Agora é hora de mostrar como um frame recém-decodificado pode ser exibido na página. Está é melhor garantir que o callback de saída do decodificador (handleFrame()) retorna rapidamente. No exemplo abaixo, ele só adiciona um frame à fila de e frames prontos para renderização. A renderização ocorre separadamente e consiste em duas etapas:

  1. Aguardando o momento certo para mostrar o frame.
  2. Desenhar o frame na tela.

Quando um frame não for mais necessário, chame close() para liberar a memória. antes do coletor de lixo, isso reduzirá a quantidade média memória usada pelo aplicativo da Web.

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let pendingFrames = [];
let underflow = true;
let baseTime = 0;

function handleFrame(frame) {
  pendingFrames.push(frame);
  if (underflow) setTimeout(renderFrame, 0);
}

function calculateTimeUntilNextFrame(timestamp) {
  if (baseTime == 0) baseTime = performance.now();
  let mediaTime = performance.now() - baseTime;
  return Math.max(0, timestamp / 1000 - mediaTime);
}

async function renderFrame() {
  underflow = pendingFrames.length == 0;
  if (underflow) return;

  const frame = pendingFrames.shift();

  // Based on the frame's timestamp calculate how much of real time waiting
  // is needed before showing the next frame.
  const timeUntilNextFrame = calculateTimeUntilNextFrame(frame.timestamp);
  await new Promise((r) => {
    setTimeout(r, timeUntilNextFrame);
  });
  ctx.drawImage(frame, 0, 0);
  frame.close();

  // Immediately schedule rendering of the next frame
  setTimeout(renderFrame, 0);
}

Dicas para desenvolvedores

Usar o Media Panel. no Chrome DevTools para ver registros de mídia e depurar WebCodecs.

Captura de tela do Media Panel para depuração do WebCodecs
Media Panel no Chrome DevTools para depuração de WebCodecs.

Demonstração

A demonstração abaixo mostra como os frames de animação de uma tela são:

  • capturada a 25 fps em uma ReadableStream por MediaStreamTrackProcessor
  • transferido para um worker da Web
  • codificado em formato de vídeo H.264
  • decodificado novamente em uma sequência de frames de vídeo
  • e renderizada na segunda tela usando transferControlToOffscreen().

Outras demonstrações

Confira também nossas outras demonstrações:

Como usar a API WebCodecs

Detecção de recursos

Para verificar o suporte ao WebCodecs:

if ('VideoEncoder' in window) {
  // WebCodecs API is supported.
}

A API WebCodecs só está disponível em contextos seguros, Portanto, a detecção falhará se self.isSecureContext for falso.

Feedback

A equipe do Chrome quer saber mais sobre suas experiências com a API WebCodecs.

Fale sobre o design da API

Alguma coisa na API não funciona como você esperava? Ou são estão faltando métodos ou propriedades que você precisa para implementar sua ideia? Tenha um pergunta ou comentário sobre o modelo de segurança? Registre um problema de especificação no repositório do GitHub correspondente ou o que você pensa sobre um problema.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação diferente das especificações? Registre um bug em new.crbug.com. Certifique-se de incluir o máximo de detalhes possível, instruções simples para reproduzindo e insira Blink>Media>WebCodecs na caixa Componentes. O Glitch é ótimo para compartilhar repetições rápidas e fáceis.

Mostrar suporte à API

Você planeja usar a API WebCodecs? Seu apoio público ajuda os a equipe do Chrome deve priorizar recursos e mostrar a outros fornecedores de navegadores é apoiá-las.

Envie e-mails para media-dev@chromium.org ou tuíte para @ChromiumDev usando a hashtag #WebCodecs e informe onde e como você o utiliza.

Imagem principal de Denise Jans em Unsplash.