Saltar al contenido principal
Web APIs

WebCodecs API: Codificación y Decodificación de Video Acelerada por Hardware en el Navegador

6 min lectura
LD
Lucio Durán
Engineering Manager & AI Solutions Architect
También disponible en: English, Italiano

Descripción General de WebCodecs

WebCodecs se sienta debajo de las APIs de media de alto nivel (<video>, MediaRecorder, MediaStream) y arriba del nivel de manipulación de bytes crudos. Te da:

  • VideoDecoder — Decodear frames de video comprimidos (H.264, VP8, VP9, AV1) en objetos VideoFrame crudos
  • VideoEncoder — Encodear objetos VideoFrame crudos en chunks comprimidos
  • AudioDecoder/AudioEncoder — Lo mismo para audio
  • VideoFrame — Un frame de video crudo que puede venir de un canvas, cámara o decoder
  • EncodedVideoChunk — Un frame de video comprimido listo para muxing

La clave: estos están acelerados por hardware. Cuando creás un VideoEncoder para H.264, el navegador delega al ASIC de encoding dedicado de la GPU (NVENC en NVIDIA, VCE en AMD, Quick Sync en Intel).

Pipeline de Encoding Básico

Empecemos con los fundamentos — encodear una animación de canse va a video H.264:

// Paso 1: Verificar soporte del codec
const support = await VideoEncoder.isConfigSupported({
 codec: 'avc1.42001f', // H.264 Baseline Level 3.1
 width: 1920,
 height: 1080,
 bitrate: 5_000_000, // 5 Mbps
 framerate: 30,
});

if (!support.supported) {
 console.error('Encoding H.264 no soportado');
}

// Paso 2: Recolectar chunks encodeados
const chunks = [];
const encoder = new VideoEncoder({
 output: (chunk, metadata) => {
 const buffer = new ArrayBuffer(chunk.byteLength);
 chunk.copyTo(buffer);

 chunks.push({
 type: chunk.type, // 'key' o 'delta'
 timestamp: chunk.timestamp,
 duration: chunk.duration,
 data: buffer,
 decoderConfig: metadata?.decoderConfig || null,
 });
 },
 error: (e) => console.error('Error de encoder:', e),
});

// Paso 3: Configurar el encoder
encoder.configure({
 codec: 'avc1.42001f',
 width: 1920,
 height: 1080,
 bitrate: 5_000_000,
 framerate: 30,
 latencyMode: 'quality',
 avc: { format: 'annexb' },
});

// Paso 4: Encodear frames desde un canvas
const canvas = document.getElementById('mi-canvas');
const ctx = canvas.getContext('2d');

for (let i = 0; i < 300; i++) { // 10 segundos a 30fps
 renderFrame(ctx, i);

 const frame = new VideoFrame(canvas, {
 timestamp: i * (1_000_000 / 30), // ¡Microsegundos!
 duration: 1_000_000 / 30,
 });

 const keyFrame = i % 90 === 0; // Keyframe cada 3 segundos
 encoder.encode(frame, { keyFrame });

 // CRÍTICO: cerrar el frame para liberar memoria GPU
 frame.close();
}

// Paso 5: Flush frames restantes
await encoder.flush();
encoder.close();

Ese frame.close() es esencial. Los VideoFrames mantienen referencias a texturas GPU. Si te olvida de cerrarlos, se filtra memoria GPU y eventualmente el navegador termina la pestaña. Sin manejo apropiado del ciclo de vida de frames, las aplicaciones de procesamiento de video comúnmente fallan después de ~200 frames.

Pipeline de Efectos de Video en Tiempo Real

A continuación, es donde WebCodecs resulta particularmente relevante — armar un pipeline de efectos en tiempo real que toma input de cámara, aplica efectos acelerados por GPU vía WebGL, y encodea el resultado:

class VideoEffectsPipeline {
 constructor(outputCanvas) {
 this.outputCanvas = outputCanvas;
 this.gl = outputCanvas.getContext('webgl2');
 this.encoder = null;
 this.running = false;
 this.setupShaders();
 }

 setupShaders() {
 const fragmentShader = `#version 300 es
 precision highp float;
 in vec2 v_texCoord;
 out vec4 outColor;
 uniform sampler2D u_texture;
 uniform float u_time;

 void main() {
 vec4 color = texture(u_texture, v_texCoord);

 // Tono sepia
 float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
 vec3 sepia = vec3(gray) * vec3(1.2, 1.0, 0.8);

 // Viñeta
 vec2 center = v_texCoord - 0.5;
 float dist = length(center);
 float vignette = smoothstep(0.7, 0.4, dist);

 // Aberración cromática animada
 float offset = sin(u_time * 2.0) * 0.002;
 float r = texture(u_texture, v_texCoord + vec2(offset, 0.0)).r;
 float b = texture(u_texture, v_texCoord - vec2(offset, 0.0)).b;

 outColor = vec4(
 mix(r, sepia.r, 0.5) * vignette,
 mix(color.g, sepia.g, 0.5) * vignette,
 mix(b, sepia.b, 0.5) * vignette,
 1.0
 );
 }
 `;
 this.program = this.compileShaderProgram(fragmentShader);
 this.timeUniform = this.gl.getUniformLocation(this.program, 'u_time');
 }

 async start(mediaStream) {
 const videoTrack = mediaStream.getVideoTracks()[0];
 const { width, height } = videoTrack.getSettings();

 const processor = new MediaStreamTrackProcessor({ track: videoTrack });
 const reader = processor.readable.getReader();

 this.encoder = new VideoEncoder({
 output: (chunk, meta) => this.handleEncodedChunk(chunk, meta),
 error: (e) => console.error('Error encoding:', e),
 });

 this.encoder.configure({
 codec: 'avc1.42001f',
 width,
 height,
 bitrate: 3_000_000,
 framerate: 30,
 latencyMode: 'realtime',
 });

 this.running = true;
 let frameCount = 0;

 while (this.running) {
 const { value: frame, done } = await reader.read();
 if (done) break;

 const processedFrame = this.applyEffect(frame, frameCount);
 frame.close();

 const keyFrame = frameCount % 90 === 0;
 this.encoder.encode(processedFrame, { keyFrame });
 processedFrame.close();

 frameCount++;
 }

 await this.encoder.flush();
 this.encoder.close();
 }

 applyEffect(inputFrame, frameIndex) {
 const gl = this.gl;

 gl.useProgram(this.program);
 gl.uniform1f(this.timeUniform, frameIndex / 30.0);

 // Subir VideoFrame como textura — zero-copy en navegadores soportados
 const texture = gl.createTexture();
 gl.bindTexture(gl.TEXTURE_2D, texture);
 gl.texImage2D(
 gl.TEXTURE_2D, 0, gl.RGBA,
 gl.RGBA, gl.UNSIGNED_BYTE,
 inputFrame // ¡VideoFrame directo como fuente de textura!
 );

 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

 const outputFrame = new VideoFrame(this.outputCanvas, {
 timestamp: inputFrame.timestamp,
 duration: inputFrame.duration,
 });

 gl.deleteTexture(texture);
 return outputFrame;
 }
}

La línea mágica es gl.texImage2D(..., inputFrame). En Chrome, cuando el buffer subyacente del VideoFrame es una textura GPU (que es así cuando viene de una cámara o decoder), esto es una operación zero-copy.

Encoding AV1: La Próxima Generación

async function getOptimalCodec(width, height) {
 const av1Config = {
 codec: 'av01.0.08M.08',
 width, height,
 bitrate: 3_000_000,
 framerate: 30,
 };

 const av1Support = await VideoEncoder.isConfigSupported(av1Config);
 if (av1Support.supported) return { ...av1Config, label: 'AV1 (HW)' };

 const vp9Config = {
 codec: 'vp09.00.31.08',
 width, height,
 bitrate: 4_000_000,
 framerate: 30,
 };

 const vp9Support = await VideoEncoder.isConfigSupported(vp9Config);
 if (vp9Support.supported) return { ...vp9Config, label: 'VP9' };

 return {
 codec: 'avc1.42001f',
 width, height,
 bitrate: 5_000_000,
 framerate: 30,
 avc: { format: 'annexb' },
 label: 'H.264',
 };
}

Comparación de Performance

Encoding de video 1080p30 por 60 segundos en un MacBook Pro 2024 (M3 Pro):

| Método | Tiempo Encoding | Pico Memoria | Uso CPU | |--------|----------------|-------------|---------| | FFmpeg.wasm (H.264) | 4 min 12s | 890MB | 100% (1 core) | | WebCodecs (H.264 HW) | 6.2s | 125MB | 15% | | WebCodecs (VP9 HW) | 8.1s | 130MB | 18% | | WebCodecs (AV1 HW) | 11.4s | 145MB | 22% | | WebCodecs (AV1 SW fallback) | 3 min 38s | 310MB | 95% (1 core) |

Los números de aceleración por hardware son impresionantes. 6.2 segundos vs 4 minutos para encoding H.264 del mismo contenido.

El Error Más Común

Los timestamps de VideoFrame son en microsegundos, no milisegundos. Este bug aparece frecuentemente en distintas codebases. Multiplicás por 1000 en vez de 1,000,000 y tu video encodeado se reproduce a 1000x de velocidad.

// MAL — timestamps en milisegundos
const frame = new VideoFrame(canvas, {
 timestamp: Date.now(), // ← milisegundos, interpretado como microsegundos
});

// BIEN — timestamps en microsegundos
const frame = new VideoFrame(canvas, {
 timestamp: performance.now() * 1000, // ← convertir ms a μs
});

WebCodecs representa un avance significativo para la plataforma web. Por primera vez, podemos hacer procesamiento de video real en el navegador sin cargar un binario WASM de muchos megas, sin saturar la CPU, y con calidad que coincide con aplicaciones nativas. Las herramientas de edición de video, streaming y comunicación que se están construyendo sobre esta API van a ser notables.

webcodecsprocesamiento-videoav1browser-apivideo-encoderaceleración-hardwarewebworkers

Herramientas mencionadas en este artículo

CloudflareProbá Cloudflare
VercelProbá Vercel
Divulgación: Algunos enlaces en este artículo son enlaces de afiliado. Si te registrás a través de ellos, puedo recibir una comisión sin costo adicional para vos. Solo recomiendo herramientas que uso y en las que confío personalmente.
Compartir
Seguime