Procesamiento de video con WebCodecs
Manipulación de componentes de transmisión de video.
Las tecnologías web modernas brindan amplias formas de trabajar con video. API de Media Stream, API de Media Recording, API de Media Source y API de WebRTC se suman a un rico conjunto de herramientas para grabar, transferir y reproducir secuencias de video. Mientras resuelven ciertas tareas de alto nivel, estas API no permiten que los programadores web trabajen con componentes individuales de una transmisión de video, como cuadros y chunks (fragmentos) de audio o video codificados sin ser multiplexados. Para obtener acceso de bajo nivel a estos componentes básicos, los desarrolladores han estado utilizando WebAssembly para incorporar códecs de audio y video al navegador. Pero dado que los navegadores modernos ya vienen con una variedad de códecs (que a menudo son acelerados por hardware), volver a empaquetarlos como WebAssembly parece una pérdida de recursos humanos e informáticos.
La API de WebCodecs elimina esta ineficiencia al brindarles a los programadores una forma de usar componentes multimedia que ya están presentes en el navegador. Específicamente:
- Decodificadores de video y audio
- Codificadores de video y audio
- Fotogramas de video sin procesar
- Decodificadores de imágenes
La API de WebCodecs es útil para aplicaciones web que requieren un control total sobre la forma en que se procesa el contenido multimedia, como editores de video, videoconferencia, transmisión de video, etc.
Estado actual #
Paso | Estado |
---|---|
1. Crear un explicador | Completado |
2. Crear borrador inicial de especificación | Completado |
3. Recopilar comentarios e iterar el diseño | Completado |
4. Prueba de origen | Completo |
5. Lanzamiento | Chrome 94 |
Flujo de trabajo de procesamiento de video #
Los fotogramas son la pieza central en el procesamiento de video. Por lo tanto, en WebCodecs, la mayoría de las clases consumen o producen fotogramas. Los codificadores de video convierten los fotogramas en chunks codificados. Los decodificadores de video hacen lo contrario.
Además, VideoFrame
es compatible con otras API web al ser CanvasImageSource
y por tener un constructor que acepta a CanvasImageSource
. Por lo tanto, se puede usar en funciones como drawImage()
y texImage2D()
. También se puede construir a partir de canvas, mapas de bits, elementos de video y otros fotogramas de video.
La API de WebCodecs funciona bien en conjunto con las clases de la API de Insertable Streams que conectan WebCodecs con pistas de transmisión multimedia.
MediaStreamTrackProcessor
divide las pistas multimedia en fotogramas individuales.MediaStreamTrackGenerator
crea una pista de medios a partir de una secuencia de fotogramas.
WebCodecs y web workers #
Por diseño, la API de WebCodecs hace todo el trabajo pesado de forma asincrónica y fuera del hilo principal. Pero dado que las retrollamadas de fotogramas y chunks a menudo se pueden llamar varias veces por segundo, pueden saturar el hilo principal y, por lo tanto, hacer que el sitio web sea menos responsivo. Por lo tanto, es preferible trasladar el manejo de fotogramas individuales y chunks codificados a un web worker.
Para ayudar a eso, ReadableStream proporciona una forma conveniente de transferir automáticamente todos los fotogramas provenientes de una pista de medios al web worker. Por ejemplo, MediaStreamTrackProcessor
se puede utilizar para obtener un ReadableStream
para una pista de transmisión multimedia procedente de la cámara web. Después de eso, la transmisión se transfiere a un web worker donde los fotogramas se leen uno por uno y se meten a la cola de VideoEncoder
.
Con HTMLCanvasElement.transferControlToOffscreen
se pueden renderizar fuera del hilo principal. Pero si todas las herramientas de alto nivel resultan inconvenientes, VideoFrame
es transferible y se puede mover entre web workers.
WebCodecs en acción #
Codificación #

Canvas
o un ImageBitmap
a la red o al almacenamientoTodo comienza con un VideoFrame
. Hay tres formas de construir fotogramas de video.
- Desde una fuente de imagen como un canvas, un mapa de bits de imagen o un elemento de video.
const cnv = document.createElement('canvas');
// dibuja algo en el canvas
…
let frame_from_canvas = new VideoFrame(cnv, { timestamp: 0 });
- Utiliza
MediaStreamTrackProcessor
para extraer fotogramas de unMediaStreamTrack
const stream = await navigator.mediaDevices.getUserMedia({ … });
const track = stream.getTracks()[0];
const media_processor = new MediaStreamTrackProcessor(track);
const reader = media_processor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done)
break;
let frame_from_camera = result.value;
}
- Crea un fotograma a partir de su renderización de píxeles binarios en un
BufferSource
const pixelSize = 4;
const init = {timestamp: 0, codedwidth: 320, codedHeight: 200, format: 'RGBA'};
let data = new Uint8Array(init.codedwidth * init.codedHeight * pixelSize);
for (let x = 0; x < init.codedwidth; x++) {
for (let y = 0; y < init.codedHeight; y++) {
let 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
}
}
let frame = new VideoFrame(data, init);
No importa de dónde vengan, los fotogramas se pueden codificar en objetos de EncodedVideoChunk
con un VideoEncoder
.
Antes de codificar, VideoEncoder
necesita dos objetos JavaScript:
- Diccionario de inicio con dos funciones para manejar chunks codificados y errores. Estas funciones están definidas por el desarrollador y no se pueden cambiar después de ser pasadas al constructor
VideoEncoder
. - Objeto de configuración del codificador, que contiene parámetros para la transmisión de video de salida. Puedes cambiar estos parámetros más tarde llamando a
configure()
.
const init = {
output: handleChunk,
error: (e) => {
console.log(e.message);
}
};
let config = {
codec: 'vp8',
width: 640,
height: 480,
bitrate: 2_000_000, // 2 Mbps
framerate: 30,
};
let encoder = new VideoEncoder(init);
encoder.configure(config);
Una vez que se hayas configurado el codificador, está listo para aceptar fotogramas mediante el método encode()
. Tanto configure()
como encode()
regresan inmediatamente sin esperar a que se complete el trabajo. Esto permite que varios fotogramas entren a la cola para ser codificados al mismo tiempo, mientras que encodeQueueSize
muestra cuántas solicitudes están esperando en la cola para que finalicen las codificaciones anteriores. Los errores se informan lanzando inmediatamente una excepción, en caso de que los argumentos o el orden de las llamadas al método infrinjan el contrato de la API, o haciendo una retrollamada de error()
para los problemas encontrados en la implementación del códec. Si la codificación es completada correctamente, la retrollamada de output() se llama con un nuevo pedazo codificado como argumento.
Otro detalle importante es que los fotogramas deben ser informados cuando ya no se necesitan llamando a close()
.
let frame_counter = 0;
const track = stream.getVideoTracks()[0];
const media_processor = new MediaStreamTrackProcessor(track);
const reader = media_processor.readable.getReader();
while (true) {
const result = await reader.read();
if (result.done)
break;
let frame = result.value;
if (encoder.encodeQueueSize > 2) {
// Too many frames in flight, encoder is overwhelmed
// let's drop this frame.
frame.close();
} else {
frame_counter++;
const insert_keyframe = (frame_counter % 150) == 0;
encoder.encode(frame, { keyFrame: insert_keyframe });
frame.close();
}
}
Finalmente, es el momento de terminar de codificar el código escribiendo una función que maneje chunks de video codificado a medida que salen del codificador. Por lo general, esta función enviaría chunks de datos a través de la red o multiplexarlos en un contenedor de medios para su almacenamiento.
function handleChunk(chunk, metadata) {
if (metadata.decoderConfig) {
// El decodificador necesita ser configurado (o volver a ser configurado) con los nuevos parámetros
// cuando la metadata tiene un nuevo decoderConfig.
// Esto usualmente sucede en el principio o cuando el codificador tiene una nueva
// especificación binaria de configuración de codec. (VideoDecoderConfig.description).
fetch('/upload_extra_data',
{
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: metadata.decoderConfig.description
});
}
// los bytes actuales de los datos codificados
let chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
let timestamp = chunk.timestamp; // tiempo de los medios en microsegundos
let is_key = chunk.type == 'key'; // también puede ser un "delta"
fetch(`/upload_chunk?timestamp=${timestamp}&type=${chunk.type}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/octet-stream' },
body: chunkData
});
}
Si en algún momento necesitas asegurarte de que se hayan completado todas las solicitudes de codificación pendientes, puedes llamar a flush()
y esperar su promesa.
await encoder.flush();
Decodificación #

Canvas
o un ImageBitmap
.La configuración de un VideoDecoder
es similar a lo que se ha hecho para el VideoEncoder
: se pasan dos funciones cuando se crea el decodificador y se pasan los parámetros del códec a configure()
.
El conjunto de parámetros del códec varía de un códec a otro. Por ejemplo, el códec H.264 puede necesitar un binary blob (blob binario) de avcC, a menos que esté codificado en el formato AnnexB (encoderConfig.avc = { format: "annexb" }
).
const init = {
output: handleFrame,
error: (e) => {
console.log(e.message);
}
};
const config = {
codec: 'vp8',
codedwidth: 640,
codedHeight: 480
};
let decoder = new VideoDecoder(init);
decoder.configure(config);
Una vez que se inicializa el decodificador, puedes comenzar a alimentarlo con objetos EncodedVideoChunk
. Para crear un chunk, necesitarás:
- Un
BufferSource
de video codificados - La marca de tiempo de inicio del chunk en microsegundos (tiempo de medios del primer fotograma codificado en el chunk)
- El tipo del chunk, cualquiera de los siguientes:
key
si el chunk se puede decodificar independientemente de los chunks anterioresdelta
si el chunk solo se puede decodificar después de que se hayan decodificado uno o más chunks anteriores
Además, cualquier chunk emitido por el codificador está listo para el decodificador. Todas las cosas mencionadas anteriormente sobre la notificación de errores y la naturaleza asincrónica de los métodos del codificador son igualmente válidas para los decodificadores.
let responses = await downloadVideoChunksFromServer(timestamp);
for (let i = 0; i < responses.length; i++) {
let 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();
Ahora es el momento de mostrar cómo se puede mostrar un fotograma recién decodificado en la página. Es mejor asegurarse de que la retrollamada de salida del decodificador (handleFrame()
) regrese rápidamente. En el siguiente ejemplo, solo agregas un fotograma a la cola de fotogramas listos para renderizar. El renderizado ocurre por separado y consta de dos pasos:
- Esperando el momento adecuado para mostrar el fotograma.
- Dibujando el fotograma en el canvas.
Una vez que ya no se necesita un fotograma, llamas a close()
para liberar la memoria subyacente antes de que el recolector de basura llegue a él, esto reducirá la cantidad promedio de memoria utilizada por la aplicación web.
let cnv = document.getElementById('canvas_to_render');
let ctx = cnv.getContext('2d');
let ready_frames = [];
let underflow = true;
let time_base = 0;
function handleFrame(frame) {
ready_frames.push(frame);
if (underflow)
setTimeout(render_frame, 0);
}
function delay(time_ms) {
return new Promise((resolve) => {
setTimeout(resolve, time_ms);
});
}
function calculateTimeTillNextFrame(timestamp) {
if (time_base == 0)
time_base = performance.now();
let media_time = performance.now() - time_base;
return Math.max(0, (timestamp / 1000) - media_time);
}
async function render_frame() {
if (ready_frames.length == 0) {
underflow = true;
return;
}
let frame = ready_frames.shift();
underflow = false;
// En base al tiempo del fotograma calculado en cuanto tiemp // o real espera
// que se necesite para mostrar el siguiente fotograma.
let time_till_next_frame = calculateTimeTillNextFrame(frame.timestamp);
await delay(time_till_next_frame);
ctx.drawImage(frame, 0, 0);
frame.close();
// Inmediatamente agenda el renderizaje del próximo fotogram//a
setTimeout(render_frame, 0);
}
Demostración #
La siguiente demostración muestra cómo son los fotogramas de animación de un canvas:
- Capturado a 25 fps en un
ReadableStream
porMediaStreamTrackProcessor
- Transferido a un web worker
- Codificado en formato de video H.264
- Decodificado de nuevo en una secuencia de fotogramas de video
- Y renderizado en el segundo canvas usando
transferControlToOffscreen()
Otras demostraciones #
Consulta también nuestras otras demostraciones:
- Decodificando gifs con ImageDecoder
- Capturar la entrada de la cámara en un archivo
- Reproducción de MP4
- Otros ejemplos
Usando la API de WebCodecs #
Detección de características #
Para comprobar la compatibilidad con WebCodecs:
if ('VideoEncoder' in window) {
// La API de WebCodecs es compatible.
}
Ten en cuenta que la API de WebCodecs solo está disponible en contextos seguros, por lo que la detección fallará si self.isSecureContext
es falso.
Retroalimentación #
El equipo de Chrome desea saber sobre tu experiencia con la API de WebCodecs.
Cuéntanos sobre el diseño de la API #
¿Hay algo en la API que no funciona como lo esperabas? ¿O faltan métodos o propiedades que necesitas para implementar tu idea? ¿Tienes alguna pregunta o comentario sobre el modelo de seguridad? Presenta un problema de especificación en el repositorio de GitHub correspondiente o agrega tus sugerencias a un problema existente.
Reportar un problema con la implementación #
¿Encontraste un error con la implementación de Chrome? ¿O la implementación es diferente de la especificación? Presenta un error en new.crbug.com. Asegúrate de incluir todos los detalles que puedas y de agregar instrucciones simples para reproducir e ingresar Blink>Media>WebCodecs
en el cuadro de Componentes. Glitch funciona muy bien para compartir reproducciones rápidas y fáciles.
Muestra tu apoyo a la API #
¿Estás pensando en utilizar la API de WebCodecs? Tu apoyo público ayuda al equipo de Chrome a priorizar funciones y muestra a otros proveedores de navegadores lo importante que es brindarles soporte.
Envía un correo electrónico a media-dev@chromium.org o envía un tweet a [@ChromiumDev] [cr-dev-twitter] usando el hashtag de #WebCodecs
y déjanos saber dónde y cómo lo estás usando.
Imagen de héroe de Denise Jans en Unsplash.