
Editar vídeo en el browser ha sido históricamente un caso extremo: demasiados datos, demasiadas operaciones por segundo, demasiado lento en la CPU. Con WebGPU, el panorama cambia. Procesar 30 frames por segundo a 1080p requiere mover y transformar 186 millones de píxeles por segundo. La GPU está diseñada exactamente para eso.

En este último artículo de la serie implementamos un pipeline de efectos de vídeo en tiempo real usando WebGPU. Veremos las piezas necesarias, los benchmarks y, sobre todo, las limitaciones reales que hay que tener en cuenta antes de embarcarse en esto en producción.

## Stack: WebGPU + WebCodecs

Para procesar vídeo con WebGPU necesitamos dos APIs:

- **WebGPU:** procesamiento de frames en la GPU
- **WebCodecs:** acceso eficiente a los frames decodificados del vídeo, sin pasar por `<canvas>`

WebCodecs es la pieza que antes faltaba. Antes de WebCodecs, acceder a los pixels de un frame de vídeo requería dibujarlo en un canvas, leer los pixels con `getImageData()`, procesarlos y volver a dibujar. Lento y con mucha duplicidad de datos.

Con WebCodecs tenemos acceso directo a los frames decodificados como `VideoFrame`, que podemos enviar directamente a la GPU.

```javascript
// Soporte requerido
if (!navigator.gpu || !window.VideoDecoder) {
  console.warn("WebGPU o WebCodecs no disponibles");
}
```

## Arquitectura del pipeline

```
Archivo de vídeo
      ↓
VideoDecoder (WebCodecs)
      ↓
VideoFrame (datos del frame)
      ↓
GPU Texture (copiamos el frame a la GPU)
      ↓
Compute/Render Shader (aplicamos efectos)
      ↓
Canvas (mostramos el resultado)
```

**La clave es que el frame viaja de la memoria del vídeo a la GPU sin pasar por la CPU para el procesamiento. La CPU solo orquesta.**

## Implementación: efecto de escala de grises en tiempo real

Empezamos con un efecto sencillo para entender el pipeline completo.

### Shader WGSL

```wgsl
@group(0) @binding(0) var videoTexture: texture_external;
@group(0) @binding(1) var outputTexture: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var texSampler: sampler;

@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
  let size = textureDimensions(outputTexture);
  if (id.x >= size.x || id.y >= size.y) { return; }

  let uv = vec2<f32>(f32(id.x) / f32(size.x), f32(id.y) / f32(size.y));

  // Muestreamos la textura externa (VideoFrame)
  let color = textureSampleBaseClampToEdge(videoTexture, texSampler, uv);

  // Luminancia perceptual (pesos ITU-R BT.709)
  let gray = dot(color.rgb, vec3<f32>(0.2126, 0.7152, 0.0722));

  textureStore(outputTexture, vec2<i32>(id.xy), vec4<f32>(gray, gray, gray, 1.0));
}
```

### Pipeline de procesamiento

```javascript
class VideoEffectPipeline {
  constructor(device, canvas) {
    this.device = device;
    this.canvas = canvas;
    this.context = canvas.getContext("webgpu");

    const format = navigator.gpu.getPreferredCanvasFormat();
    this.context.configure({ device, format });
  }

  async init(shaderCode) {
    const { device } = this;

    this.outputTexture = device.createTexture({
      size: [this.canvas.width, this.canvas.height],
      format: "rgba8unorm",
      usage: GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.TEXTURE_BINDING,
    });

    this.sampler = device.createSampler({
      minFilter: "linear",
      magFilter: "linear",
    });

    const shaderModule = device.createShaderModule({ code: shaderCode });

    this.pipeline = device.createComputePipeline({
      layout: "auto",
      compute: { module: shaderModule, entryPoint: "main" },
    });
  }

  processFrame(videoFrame) {
    const { device } = this;

    // Importar VideoFrame como textura GPU (zero-copy cuando es posible)
    const videoTexture = device.importExternalTexture({ source: videoFrame });

    const bindGroup = device.createBindGroup({
      layout: this.pipeline.getBindGroupLayout(0),
      entries: [
        { binding: 0, resource: videoTexture },
        { binding: 1, resource: this.outputTexture.createView() },
        { binding: 2, resource: this.sampler },
      ],
    });

    const encoder = device.createCommandEncoder();
    const pass = encoder.beginComputePass();
    pass.setPipeline(this.pipeline);
    pass.setBindGroup(0, bindGroup);
    pass.dispatchWorkgroups(
      Math.ceil(this.canvas.width / 16),
      Math.ceil(this.canvas.height / 16)
    );
    pass.end();

    // Render pass para mostrar en canvas
    const renderPass = encoder.beginRenderPass({
      colorAttachments: [
        {
          view: this.context.getCurrentTexture().createView(),
          loadOp: "clear",
          storeOp: "store",
        },
      ],
    });
    // ... blit de outputTexture al canvas
    renderPass.end();

    device.queue.submit([encoder.finish()]);
    videoFrame.close(); // Liberar memoria del frame
  }
}
```

### Loop de reproducción

```javascript
async function startVideoProcessing(videoFile, canvasElement) {
  const adapter = await navigator.gpu.requestAdapter();
  const device = await adapter.requestDevice();

  const pipeline = new VideoEffectPipeline(device, canvasElement);
  await pipeline.init(GRAYSCALE_SHADER);

  const decoder = new VideoDecoder({
    output: frame => {
      pipeline.processFrame(frame);
    },
    error: e => console.error("Decode error:", e),
  });

  decoder.configure({
    codec: "avc1.42001f", // H.264 baseline
    hardwareAcceleration: "prefer-hardware",
  });

  // Leer chunks del archivo y enviarlos al decoder
  const reader = videoFile.stream().getReader();
  // ... envío de chunks al decoder
}
```

## Benchmarks: CPU vs WebGPU para efectos de vídeo

Aplicando efecto de escala de grises a vídeo 1080p/30fps:

| Método                     | Tiempo por frame | FPS sostenibles | Uso de CPU |
| -------------------------- | ---------------- | --------------- | ---------- |
| Canvas 2D (`getImageData`) | ~45ms            | ~8 fps          | 85%        |
| WebGL (fragment shader)    | ~4ms             | ~60 fps         | 15%        |
| WebGPU (compute shader)    | ~2ms             | ~60 fps         | 8%         |

El beneficio de WebGPU frente a WebGL no es solo velocidad: la CPU queda más libre para otras tareas (audio, UI, lógica de aplicación).

Para efectos más complejos (corrección de color con LUT, efectos de partículas, composición multicapa), la diferencia entre WebGL y WebGPU se amplía.

## Efectos que podemos aplicar con este patrón

El patrón es siempre el mismo: un shader que recibe el frame y escribe el resultado. Algunos efectos que se pueden implementar:

### Corrección de color

```wgsl
// Curvas de color con LUT (Look-Up Table)
let lut = textureSample(lutTexture, lutSampler, color.rgb);
textureStore(output, coords, vec4<f32>(lut.rgb, color.a));
```

### Chroma key (green screen)

```wgsl
let green = vec3<f32>(0.0, 1.0, 0.0);
let diff = distance(color.rgb, green);
let alpha = select(0.0, 1.0, diff > 0.3);
textureStore(output, coords, vec4<f32>(color.rgb, alpha));
```

### Motion blur temporal

Promedio de N frames anteriores, lo que requiere mantener un buffer de frames en GPU.

## Limitaciones reales

### WebCodecs y cross-origin

Los `VideoFrame` importados desde vídeos cross-origin tienen restricciones. En producción, los vídeos deben estar en el mismo origen o con CORS configurado.

### Codecs soportados

H.264 y VP8/VP9 tienen soporte amplio. AV1 varía por dispositivo y sistema operativo. Podemos consultar `VideoDecoder.isConfigSupported()` antes de configurar.

```javascript
const support = await VideoDecoder.isConfigSupported({
  codec: "av01.0.04M.08",
  hardwareAcceleration: "prefer-hardware",
});

if (!support.supported) {
  // Fallback a H.264
}
```

### Sincronización de audio

WebGPU procesa vídeo sin saber nada del audio. La sincronización audio-vídeo es responsabilidad nuestra. Hay que medir el tiempo de procesamiento de cada frame y ajustar el timing de presentación.

### Soporte de navegadores

WebCodecs y WebGPU están disponibles en Chrome y Edge. Safari tiene soporte parcial. Firefox no tiene ninguno de los dos en versiones estables.

### Memoria GPU

Vídeos largos o múltiples streams simultáneos pueden agotar la memoria de la GPU. Hay que liberar los `VideoFrame` con `frame.close()` tan pronto como sea posible.

## ¿Cuándo tiene sentido en producción?

La combinación WebGPU + WebCodecs es madura para:

- Editores de vídeo web (Clipchamp, CapCut Web) en navegadores Chromium
- Herramientas de videoconferencia con efectos de fondo (blur, virtual background)
- Procesamiento de vídeo antes de subida (compresión, marca de agua, recorte)
- Visualizadores de vídeo con efectos de color en tiempo real

No está lista para:

- Aplicaciones que necesiten soporte en Firefox o Safari
- Edición de vídeos de más de 4K sin gestión explícita de memoria

## Conclusión

El procesamiento de vídeo en el browser con WebGPU + WebCodecs es real y funciona. Con 2ms por frame en 1080p, hay margen para efectos complejos dentro del presupuesto de 16ms de un frame a 60fps.

El ecosistema todavía está madurando: soporte limitado en Firefox y Safari, APIs que cambian, poca documentación. Pero las bases están asentadas y los casos de uso son claros.

Con esto cerramos la serie. Hemos recorrido WebGPU desde los fundamentos hasta tres casos de uso reales: procesamiento de imágenes, ML en el browser y edición de vídeo. En todos, el patrón es el mismo: mover el trabajo pesado a la GPU y dejar que la CPU orqueste.
