Manipular componentes de stream de vídeo.
As tecnologias modernas da Web oferecem diversas maneiras de trabalhar com vídeos. A API Media Stream, a API Media Recording, a API Media Source e a API WebRTC formam um conjunto de ferramentas para gravar, transferir e reproduzir streams de vídeo. Ao resolver determinadas tarefas de alto nível, essas APIs não permitem que os programadores da Web trabalhem com componentes individuais de um stream de vídeo, como frames e blocos de vídeo ou áudio codificados sem separação. Para ter acesso de baixo nível a esses componentes básicos, os desenvolvedores têm usado o WebAssembly para trazer codecs de vídeo e áudio para o navegador. No entanto, como os navegadores modernos já vêm com vários codecs (que geralmente são acelerados pelo hardware), reempacotá-los como WebAssembly parece um desperdício de recursos 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 áudio e vídeo
- Codificadores de áudio e vídeo
- 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 de vídeo etc.
Fluxo de trabalho de processamento de vídeo
Os frames são o ponto central do processamento de vídeo. Assim, em WebCodecs, a maioria das classes consome ou produz frames. Os codificadores de vídeo convertem frames em fragmentos codificados. Os decodificadores de vídeo fazem o contrário.
Além disso, a VideoFrame
funciona bem com outras APIs da Web, sendo um CanvasImageSource
e tendo um construtor que aceita CanvasImageSource
.
Assim, ele pode ser usado em funções como drawImage()
e texImage2D()
. Além disso, ele pode ser criado 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 WebCodecs a faixas de stream de mídia.
MediaStreamTrackProcessor
divide as faixas de mídia em frames individuais.MediaStreamTrackGenerator
cria uma faixa de mídia a partir de 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. No entanto, como os callbacks de frame e de bloco podem ser chamados várias vezes por segundo, eles podem congestionar a linha de execução principal e tornar o site menos responsivo. Portanto, é preferível mover o processamento de frames individuais e blocos codificados para um web worker.
Para ajudar nisso, o ReadableStream
oferece uma maneira conveniente de transferir automaticamente todos os frames de uma faixa
de mídia para o worker. Por exemplo, MediaStreamTrackProcessor
pode ser usado para receber um
ReadableStream
para uma faixa de transmissão de mídia vinda da webcam. Depois disso,
o stream é transferido para um worker da Web em que os frames são lidos um por um e colocados na fila
em um VideoEncoder
.
Com HTMLCanvasElement.transferControlToOffscreen
, até mesmo a renderização pode ser feita fora da linha de execução principal. No entanto, se todas as ferramentas de alto nível forem inconvenientes, o próprio VideoFrame
será transferível e poderá ser movido entre workers.
WebCodecs em ação
Codificação
Tudo começa com um VideoFrame
.
Há três maneiras de construir frames de vídeo.
De uma fonte 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 umaMediaStreamTrack
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; }
Crie um frame a partir da representação binária de pixels 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 eles vêm, os frames podem ser codificados em
objetos EncodedVideoChunk
com um VideoEncoder
.
Antes da codificação, VideoEncoder
precisa receber dois objetos JavaScript:
- Inicialização do dicionário com duas funções para processar blocos e
erros codificados. Essas funções são definidas pelo desenvolvedor e não podem ser alteradas depois
de serem transmitidas para o construtor
VideoEncoder
. - Objeto de configuração do codificador, que contém parâmetros para o stream de vídeo
de saída. É possível mudar esses parâmetros mais tarde chamando
configure()
.
O método configure()
vai gerar NotSupportedError
se a configuração não tiver
suporte do navegador. Recomendamos chamar o método estático
VideoEncoder.isConfigSupported()
com a configuração para verificar com antecedência se
ela tem suporte e aguardar 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 que o codificador é configurado, ele está pronto para aceitar frames pelo método encode()
.
configure()
e encode()
retornam imediatamente, sem aguardar a
conclusão do trabalho. Ele permite que vários frames sejam enfileirados para codificação ao
mesmo tempo, enquanto encodeQueueSize
mostra quantos solicitações estão aguardando na fila
para que as codificações anteriores sejam concluídas.
Os erros são informados gerando uma exceção imediatamente, caso os argumentos
ou a ordem das chamadas de método violem o contrato da API, ou chamando o callback error()
para problemas encontrados na implementação do codec.
Se a codificação for concluída, o callback output()
será chamado com um novo bloco codificado como argumento.
Outro detalhe importante é que os frames precisam ser informados quando não
são mais necessários 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 o código de codificação escrevendo uma função que processa fragmentos de vídeo codificado conforme eles saem do codificador. Normalmente, essa função envia blocos de dados pela rede ou os muxa em um contêiner de mídia 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 verificar se todos os pedidos de codificação pendentes foram
concluídos, chame flush()
e aguarde a promessa.
await encoder.flush();
Decodificação
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 os parâmetros
do codec 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 seja codificado no chamado formato 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ê precisa 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 do bloco)
- o tipo do bloco, um dos seguintes:
key
, se o fragmento puder ser decodificado independentemente dos fragmentos anterioresdelta
, se o bloco só puder ser decodificado depois que um ou mais blocos anteriores forem decodificados
Além disso, todos os fragmentos emitidos pelo codificador estão prontos para o decodificador no estado em que se encontram. Tudo o que foi dito acima sobre a geração de 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 mostrado na página. É
melhor garantir que o callback de saída do decodificador (handleFrame()
)
seja retornado rapidamente. No exemplo abaixo, ela apenas adiciona um frame à fila de
frames prontos para renderização.
A renderização acontece separadamente e consiste em duas etapas:
- Aguardando o momento certo para mostrar o frame.
- Desenho do frame na tela.
Quando um frame não for mais necessário, chame close()
para liberar a memória subjacente
antes que o coletor de lixo a alcance. Isso vai reduzir a quantidade média de
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
Use o Painel de mídia no Chrome DevTools para conferir os registros de mídia e depurar 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
porMediaStreamTrackProcessor
- transferido para um worker da Web
- codificado em formato de vídeo H.264
- decodificada 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 decodificar GIFs com o ImageDecoder
- Capturar a entrada da câmera em um arquivo
- Reprodução de MP4
- Outras amostras
Como usar a API WebCodecs
Detecção de recursos
Para verificar o suporte a WebCodecs:
if ('VideoEncoder' in window) {
// WebCodecs API is supported.
}
A API WebCodecs só está disponível em contextos seguros.
Portanto, a detecção vai falhar se self.isSecureContext
for falso.
Feedback
A equipe do Chrome quer saber sobre sua experiência com a API WebCodecs.
Conte sobre o design da API
Há algo na API que não funciona como esperado? Ou há métodos ou propriedades ausentes que você precisa para implementar sua ideia? Tem uma pergunta ou comentário sobre o modelo de segurança? Envie um problema de especificação no repositório do GitHub correspondente ou adicione sua opinião a um problema existente.
Informar um problema com a implementação
Você encontrou um bug na implementação do Chrome? Ou a implementação
é diferente da especificação? Registre um bug em new.crbug.com.
Inclua o máximo de detalhes possível, instruções simples para
reprodução e insira Blink>Media>WebCodecs
na caixa Components.
O Glitch é ótimo para compartilhar reprosagens rápidas e fáceis.
Mostrar suporte à API
Você planeja usar a API WebCodecs? Seu apoio público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegadores a importância de oferecer suporte a eles.
Envie e-mails para media-dev@chromium.org ou envie um tweet
para @ChromiumDev usando a hashtag
#WebCodecs
e informe onde e como você está usando.
Imagem principal de Denise Jans no Unsplash.