Manipular componentes de stream de vídeo.
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
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 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; }
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
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 anterioresdelta
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:
- Aguardando o momento certo para mostrar o frame.
- 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.
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
- 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 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 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.