WebCodecs API: Codifica e Decodifica Video Accelerata via Hardware nel Browser
Panoramica di WebCodecs
WebCodecs si posiziona sotto le API media di alto livello (<video>, MediaRecorder, MediaStream) e sopra il livello di manipolazione byte grezzi. Ti dà:
- VideoDecoder — Decodificare frame video compressi in oggetti
VideoFramegrezzi - VideoEncoder — Encodare oggetti
VideoFramegrezzi in chunk compressi - VideoFrame — Un frame video grezzo proveniente da canvas, camera o decoder
- EncodedVideoChunk — Un frame video compresso pronto per il muxing
La chiave: questi sono accelerati via hardware. Quando crei un VideoEncoder per H.264, il browser delega all'ASIC di encoding dedicato della GPU.
Pipeline di Encoding Base
// Passo 1: Verificare supporto codec
const support = await VideoEncoder.isConfigSupported({
codec: 'avc1.42001f',
width: 1920,
height: 1080,
bitrate: 5_000_000,
framerate: 30,
});
// Passo 2: Raccogliere chunk encodati
const chunks = [];
const encoder = new VideoEncoder({
output: (chunk, metadata) => {
const buffer = new ArrayBuffer(chunk.byteLength);
chunk.copyTo(buffer);
chunks.push({
type: chunk.type,
timestamp: chunk.timestamp,
duration: chunk.duration,
data: buffer,
decoderConfig: metadata?.decoderConfig || null,
});
},
error: (e) => console.error('Errore encoder:', e),
});
// Passo 3: Configurare l'encoder
encoder.configure({
codec: 'avc1.42001f',
width: 1920,
height: 1080,
bitrate: 5_000_000,
framerate: 30,
latencyMode: 'quality',
avc: { format: 'annexb' },
});
// Passo 4: Encodare frame da un canvas
const canvas = document.getElementById('mio-canvas');
const ctx = canvas.getContext('2d');
for (let i = 0; i < 300; i++) {
renderFrame(ctx, i);
const frame = new VideoFrame(canvas, {
timestamp: i * (1_000_000 / 30), // Microsecondi!
duration: 1_000_000 / 30,
});
const keyFrame = i % 90 === 0;
encoder.encode(frame, { keyFrame });
// CRITICO: chiudere il frame per liberare memoria GPU
frame.close();
}
await encoder.flush();
encoder.close();
Quella chiamata frame.close() è essenziale. I VideoFrame mantengono riferimenti a texture GPU. Se dimentichi di chiuderli, perdi memoria GPU e alla fine il browser uccide il tuo tab.
Pipeline Effetti Video in Tempo Reale
Ecco dove WebCodecs diventa davvero interessante — costruire una pipeline di effetti in tempo reale:
class VideoEffectsPipeline {
constructor(outputCanvas) {
this.outputCanvas = outputCanvas;
this.gl = outputCanvas.getContext('webgl2');
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);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
vec3 sepia = vec3(gray) * vec3(1.2, 1.0, 0.8);
vec2 center = v_texCoord - 0.5;
float dist = length(center);
float vignette = smoothstep(0.7, 0.4, dist);
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);
}
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('Errore encoding:', e),
});
this.encoder.configure({
codec: 'avc1.42001f',
width, height,
bitrate: 3_000_000,
framerate: 30,
latencyMode: 'realtime',
});
let frameCount = 0;
this.running = true;
while (this.running) {
const { value: frame, done } = await reader.read();
if (done) break;
const processedFrame = this.applyEffect(frame, frameCount);
frame.close();
this.encoder.encode(processedFrame, { keyFrame: frameCount % 90 === 0 });
processedFrame.close();
frameCount++;
}
await this.encoder.flush();
this.encoder.close();
}
applyEffect(inputFrame, frameIndex) {
const gl = this.gl;
// Caricare VideoFrame come texture — zero-copy sui browser supportati
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 direttamente come sorgente texture!
);
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 riga magica è gl.texImage2D(..., inputFrame). Su Chrome, quando il buffer sottostante del VideoFrame è una texture GPU, questa è un'operazione zero-copy.
Confronto Prestazioni
Encoding video 1080p30 per 60 secondi su MacBook Pro 2024 (M3 Pro):
| Metodo | Tempo Encoding | Picco 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) |
I numeri dell'accelerazione hardware sono impressionanti. 6,2 secondi contro 4 minuti per l'encoding H.264 dello stesso contenuto.
Il Bug Più Grande
I timestamp dei VideoFrame sono in microsecondi, non millisecondi. Questo bug appare frequentemente in diverse codebase.
// SBAGLIATO — timestamp in millisecondi
const frame = new VideoFrame(canvas, {
timestamp: Date.now(), // ← millisecondi, interpretati come microsecondi
});
// CORRETTO — timestamp in microsecondi
const frame = new VideoFrame(canvas, {
timestamp: performance.now() * 1000, // ← convertire ms in μs
});
WebCodecs rappresenta un avanzamento significativo per la piattaforma web. Per la prima volta, possiamo fare elaborazione video reale nel browser senza caricare un binary WASM multi-megabyte, senza inchiodare la CPU, e con qualità che corrisponde alle applicazioni native. Gli strumenti di editing video, streaming e comunicazione che si stanno costruendo su questa API saranno straordinari.