ffmpeg-playground

Romper el video
para entender la máquina

Un taller de 3 horas donde no se aprende ffmpeg. Se aprende ciencias de la computación, y ffmpeg es el bisturí. El datamoshing no es el objetivo: es la prueba de que entendiste cómo funciona un decodificador por dentro.

Duración
3 horas
Nivel
Cero conocimiento de video. Terminal básica.
Herramientas
ffmpeg, ffprobe, ffplay, un editor de texto
Te llevás
Un clip moshado de 10 segundos y la razón exacta de por qué se ve así
Esto es el final del módulo 4. Dos clips sin nada que ver entre sí, pegados, y al segundo le borramos el fotograma clave. Los vectores de movimiento del segundo se aplican sobre los píxeles del primero. Ningún byte está corrupto.

01 La idea

Casi todo curso de programación enseña conceptos con ejemplos que no le importan a nadie: listas de nombres, cuentas bancarias de mentira, foo y bar. Después de dos horas nadie recuerda qué era una máquina de estados.

Este taller invierte el orden. Primero rompés algo, ves el resultado en pantalla, y después descubrís que lo que rompiste tiene un nombre técnico y aparece en el diseño de todos los sistemas que usás.

El video comprimido es el material perfecto para eso, por tres razones:

02 Preparación

Antes de arrancar, cada persona necesita esto resuelto. No gastes tiempo del taller instalando cosas.

Instalar

# Debian / Ubuntu
sudo apt install ffmpeg

# macOS
brew install ffmpeg

# Verificar (necesitás 6.x o superior)
ffmpeg -version

Todos los comandos de este taller fueron probados en ffmpeg 6.1.1 con libx264 y libxvid compilados. Chequealo:

ffmpeg -hide_banner -encoders | grep -E 'libx264|mpeg4'

Traer material

Si alguien llega con las manos vacías, ffmpeg genera material solo. Nadie se queda afuera:

ffmpeg -f lavfi -i "testsrc2=s=640x360:r=25:d=10" -pix_fmt yuv420p a.mp4
ffmpeg -f lavfi -i "mandelbrot=s=640x360:r=25" -t 10 -pix_fmt yuv420p b.mp4

04 El taller

Siete módulos, 3 horas, solo terminal. Cada módulo tiene la misma estructura: un concepto de ciencias de la computación, algo que hacés con las manos, y el momento en que las dos cosas encajan.

Módulo 0

Nada es un archivo de video

15 min

Concepto Capas de abstracción, encapsulamiento

ffprobe -hide_banner clip.mp4

# Cambiar de contenedor sin tocar los píxeles
ffmpeg -i clip.mp4 -c copy clip.mkv

Esa segunda operación tarda menos de un segundo y no pierde absolutamente nada de calidad. Comparala con una recodificación real y que alguien explique por qué la diferencia es de dos órdenes de magnitud.

Hay tres contratos apilados y uno solo contiene imágenes:

  1. Contenedor (mp4, mkv, avi): quién va primero, dónde está cada pista, cuánto dura. Es un índice.
  2. Códec (h264, mpeg4, av1): cómo se comprimieron los píxeles.
  3. Bitstream: los bytes concretos que el decodificador va a leer.

El momento Renombrar .mp4 a .mkv rompe el archivo; remuxear no. Probá las dos cosas. "Formato de archivo" no es una cosa: son al menos tres contratos independientes, y confundirlos es el 80% de los problemas de video que alguien va a googlear en su vida.

Módulo 1

Todo son bytes

20 min

Concepto Representación de datos, framing, registros de tamaño fijo

ffmpeg -f rawvideo -pix_fmt gray -s 128x128 -r 12 \
       -i /usr/lib/x86_64-linux-gnu/libavcodec.so.60 \
       -frames:v 42 -r 12 -c:v libx264 -crf 34 -pix_fmt yuv420p \
       -vf "scale=480:-2:flags=neighbor" binario.mp4

ffplay binario.mp4
libavcodec reproduciéndose a sí mismo. Eso que ves es el código compilado del decodificador de video, interpretado como píxeles por el decodificador de video. Se distinguen tres texturas: una banda de ruido denso arriba (tablas de datos), un patrón regular de puntos en el medio (instrucciones de máquina, alineadas a intervalos fijos) y estructura más sucia abajo.

Elegimos la biblioteca y no el ejecutable /usr/bin/ffmpeg por una razón que vale la pena contar en clase: ese ejecutable pesa 342 KB porque está enlazado dinámicamente, o sea 5 fotogramas y se termina. El código de verdad vive en libavcodec, 15,8 MB. Enlazado dinámico, explicado sin una sola diapositiva.

El comando además tira una advertencia si lo dejás leer hasta el final, y es la parte más valiosa:

[rawvideo] Invalid buffer size, packet size 14808 < expected frame_size 65536

El archivo no se dividía en fotogramas enteros. Un fotograma de 256×256 en escala de grises son exactamente 65.536 bytes y sobraron 14.808. Ahí aparece el problema del framing: cómo sabe un lector dónde termina un registro y empieza el siguiente. Un contenedor de video existe, entre otras cosas, para responder esa pregunta que acá tuvimos que contestar a mano.

Después, mirar píxeles como números:

ffmpeg -f lavfi -i "testsrc2=s=640x360:r=25:d=1" \
       -vf "crop=16:10:74:40,datascope=mode=color2" \
       -frames:v 1 datascope.png
Cuadrícula de píxeles con sus valores YUV en hexadecimal; la mayoría son 51/5A/F0 (rojo) y en la esquina superior derecha aparecen BC/1D/A2 (cian)
Un recorte de 16×10 píxeles con su valor numérico encima. Casi todos son 51 5A F0 — el rojo de la barra. Arriba a la derecha aparecen BC 1D A2: ahí empieza la barra siguiente. Ese es el borde entre dos colores, leído como números.

Tema de discusión: por qué el video se guarda en YUV y no en RGB, y por qué el croma se submuestrea. La respuesta no es informática, es biológica — el ojo humano tiene mucho menos resolución para color que para luminancia, así que el formato tira información que no vas a poder ver. Es la primera aparición de una idea que gobierna todo el resto del taller.

El momento No existe "el archivo de imagen". Existen bytes y un acuerdo sobre cómo interpretarlos. Cambiás el acuerdo y los mismos bytes son otra cosa.

Módulo 2

La compresión es una perilla

25 min

Concepto Tasa–distorsión, con y sin pérdida, teoría de la información

for b in 50k 200k 1000k; do
  ffmpeg -y -i clip.mp4 -c:v libx264 -b:v $b -pix_fmt yuv420p q_$b.mp4
  ffmpeg -i q_$b.mp4 -i clip.mp4 -lavfi psnr -f null - 2>&1 | grep average
done

Resultado medido sobre un clip de prueba de 3 segundos:

BitrateTamañoPSNR
50 kbps21.589 bytes28,26 dB
200 kbps68.949 bytes37,62 dB
1000 kbps337.237 bytes49,22 dB

Que cada uno grafique tamaño contra calidad. No es una recta: es una curva logarítmica con rendimientos decrecientes. Multiplicar el tamaño por 16 no multiplicó la calidad por 16. Esa curva tiene nombre — tasa–distorsión — y es la razón por la que ninguna plataforma de streaming te da el máximo bitrate posible.

Después, el experimento que cierra el módulo. Mismo codificador, mismos parámetros, misma duración, misma resolución. Solo cambia el contenido:

# Ruido aleatorio puro
ffmpeg -f lavfi -i "nullsrc=s=640x360:r=25:d=5,geq=random(1)*255:128:128" \
       -c:v libx264 -crf 23 -pix_fmt yuv420p ruido.mp4

# Negro absoluto
ffmpeg -f lavfi -i "color=black:s=640x360:r=25:d=5" \
       -c:v libx264 -crf 23 -pix_fmt yuv420p negro.mp4

ls -l ruido.mp4 negro.mp4

ruido.mp410.815.582 bytes

negro.mp45.752 bytes

Factor: 1880×

El momento Comprimir es apostar a que los datos son predecibles. El ruido aleatorio es lo único que no se puede comprimir, porque no hay nada que predecir. Y de ahí sale, sin escala intermedia, la respuesta a una pregunta que parece de otra materia: por qué un archivo cifrado no se comprime. Un buen cifrado produce salida indistinguible del ruido. Si tu archivo cifrado se comprimiera, tu cifrado sería malo.

Módulo 3

El tiempo también se comprime

25 min

Concepto Redundancia temporal, codificación delta, el GOP como máquina de estados

Hasta acá comprimimos cada imagen por separado. Pero el fotograma 100 de un video se parece muchísimo al 99. Ahí hay otra apuesta para hacer.

# Ver el tipo de cada fotograma
ffprobe -select_streams v -show_entries frame=pict_type -of csv=p=0 clip.mp4 | tr -d '\n'
IPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP...

Ahora la medición. Mismo material, misma calidad, única diferencia: cada cuántos fotogramas insertamos una imagen completa.

# -g 1: cada fotograma es independiente
ffmpeg -y -f lavfi -i "testsrc2=s=640x360:r=25:d=5" -c:v mpeg4 -g 1   -bf 0 gop1.avi

# -g 250: un fotograma completo y 249 diferencias
ffmpeg -y -f lavfi -i "testsrc2=s=640x360:r=25:d=5" -c:v mpeg4 -g 250 -bf 0 gop250.avi

ls -l gop1.avi gop250.avi

gop1.avi935.976 bytes

gop250.avi218.944 bytes

Factor: 4,3× — y es el mismo video

Qué es cada letra:

  • I (intra) — una imagen completa. Se decodifica sola, sin depender de nada. Es un punto de entrada.
  • P (predicho)no contiene una imagen. Contiene instrucciones: "tomá estos bloques de 16×16 del fotograma anterior, movelos a estas coordenadas, y sumales esta corrección chica". Guarda movimiento, no imagen.
  • B (bidireccional) — igual que P pero puede referirse al pasado y al futuro. Sí: para decodificarlo hay que haber decodificado un fotograma que todavía no se mostró.

El momento El decodificador es una máquina de estados. Cada fotograma P es una transición que asume que el estado anterior es correcto. Nadie verifica esa suposición: verificarla costaría exactamente lo mismo que no comprimir. Toda la eficiencia del video moderno se apoya en una suposición no verificada. Guardá esa frase, porque el módulo que sigue es lo que pasa cuando la suposición es falsa.

Módulo 4

Datamosh

45 min — el módulo central

Concepto Propagación de errores, tolerancia a fallos, estado inválido

Rompamos la suposición del módulo 3. Vamos a pegar dos clips y después borrar todos los fotogramas clave menos el primero. Sin punto de entrada propio, las instrucciones de movimiento del clip B se van a aplicar sobre los píxeles del clip A.

Paso 1 — Normalizar

ffmpeg -y -i a.mp4 -c:v mpeg4 -vtag xvid -q:v 3 -g 250 -bf 0 -an a.avi
ffmpeg -y -i b.mp4 -c:v mpeg4 -vtag xvid -q:v 3 -g 250 -bf 0 -an b.avi

-bf 0 desactiva los fotogramas B. Con predicción bidireccional el efecto se ensucia, porque parte del movimiento viene del futuro. Usamos MPEG-4 en AVI porque es un formato viejo y crédulo: hace pocas verificaciones y por eso se rompe lindo.

Paso 2 — Pegar sin recodificar

printf "file 'a.avi'\nfile 'b.avi'\n" > list.txt
ffmpeg -y -f concat -safe 0 -i list.txt -c copy joined.avi

ffprobe -select_streams v -show_entries frame=pict_type -of csv=p=0 joined.avi | tr -d '\n'
IPPPPPPPPPPPPPPPPPP...PPPIPPPPPPPPPPPPPPP...    ← dos I, una por clip

Paso 3 — El mosh

ffmpeg -y -i joined.avi -c copy -bsf:v noise=drop='key*gt(n\,0)' moshed.avi

ffprobe -select_streams v -show_entries frame=pict_type -of csv=p=0 moshed.avi | tr -d '\n'
IPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP...       ← quedó una sola I

Esa expresión dice: descartá todo paquete que sea clave (key) y no sea el fotograma cero (gt(n,0)). Un solo punto de entrada para los dos clips.

Mirá el resultado antes de exportar:

ffplay moshed.avi

Paso 4 — Fijar la alucinación

ffmpeg -y -err_detect ignore_err -ec favor_inter -i moshed.avi \
       -c:v libx264 -crf 20 -pix_fmt yuv420p final.mp4

Este paso no crea el glitch: lo graba. El glitch ocurre en el decodificador, en vivo, cada vez que se reproduce. Acá lo estamos filmando para poder compartirlo.

Control El mismo corte, intacto. Cada clip tiene su fotograma clave, así que el segundo entra limpio. Un corte común.
Moshado Un solo byte de diferencia en la decisión: sin el segundo fotograma clave. El mandelbrot llega con instrucciones de movimiento pero sin imagen propia, y las aplica sobre las barras de color.

Los dos clips son la misma ventana temporal del mismo material, codificados con los mismos parámetros. La única diferencia es cuáles paquetes llegaron al decodificador.

La discusión — acá aterriza la parte de computación

La estética es una función de producción

-ec favor_inter significa error concealment: favor inter prediction. Es una función seria, pensada para que un video sobreviva a una red que pierde paquetes: si falta información, rellenala usando predicción entre fotogramas. Es exactamente el mecanismo del datamosh. La estética glitch y el mecanismo de resiliencia son la misma línea de código. Estamos usando una función de producción fuera de su propósito.

¿Por qué no explota?

Cualquier parser de JSON, cualquier handshake de TLS, cualquier verificador de firma aborta ante datos inesperados. Un decodificador de video hace lo contrario a propósito: en una videollamada, perder un paquete tiene que degradar la imagen, no cortar la llamada. La misma decisión de diseño — cuánta permisividad tolerar — tiene respuestas opuestas según el modelo de amenaza. En video, ser permisivo es la función. En criptografía, ser permisivo es la vulnerabilidad.

Nada está corrupto

Este es el punto más importante del taller y el más fácil de pasar por alto. Cada byte que quedó en el archivo es válido. No corrompimos ningún dato. Lo que es inválido es la secuencia: la máquina de estados recibió instrucciones para un estado en el que nunca estuvo.

Y esa es la forma exacta de toda una familia de bugs reales: use-after-free, lecturas parciales, condiciones de carrera, paquetes reenviados en un protocolo sin protección de replay. En todos, los datos son válidos y el orden no. Acá lo podés ver arrastrarse en pantalla.

Variantes, 5 minutos cada una

# Fotogramas B: ahora los píxeles vienen también del futuro
ffmpeg -y -i a.mp4 -c:v mpeg4 -vtag xvid -q:v 3 -g 250 -bf 2 a_bframes.avi

# Corromper bytes en vez de descartar paquetes. Compará la firma visual:
# corrupción de bytes = bloques; pérdida de paquetes = arrastre
ffmpeg -y -i joined.avi -c copy -bsf:v noise=amount=3 bytes_roto.avi

# Descarte aleatorio: 1 de cada 5 paquetes
ffmpeg -y -i joined.avi -c copy -bsf:v noise=dropamount=5 tartamudo.avi
Corrupción de bytes (noise=amount=3), no pérdida de paquetes. Compará la firma visual con el mosh de arriba: acá aparecen bloques sueltos de color equivocado, porque se rompieron coeficientes individuales dentro de macrobloques de 16×16. El mosh, en cambio, arrastra: ahí los bloques estaban intactos y lo que faltaba era el punto de partida. Dos fallas distintas, dos estéticas distintas.

Mientras corre ese comando, mirá lo que escupe ffmpeg por stderr:

[mpeg4] ac-tex damaged at 22 22
[mpeg4] Error at MB: 924
[mpeg4] illegal dc vlc
[mpeg4] 2. marker bit missing in 3. esc

Ese es el decodificador narrando su propio fracaso en tiempo real, macrobloque por macrobloque. "MB" es macroblock. Ahí se ve que no hay magia: hay un parser encontrando bits que no cierran, decidiendo seguir igual, y dejando en pantalla lo último que tenía.

El momento Cada variante rompe el video de una manera distinta y reconocible. Con práctica, mirando un glitch se puede deducir qué capa falló. Eso es depurar: leer el síntoma para inferir la causa. Recién estuviste leyendo un core dump que además era lindo.

Módulo 5

Los grafos de filtros son programas

25 min

Concepto DAG, dataflow, pipelines

ffmpeg -i clip.mp4 -filter_complex "\
  [0:v]split=2[base][fx]; \
  [fx]hue=s=0,edgedetect,negate[ghost]; \
  [base][ghost]blend=all_mode=difference[out]" \
  -map "[out]" -c:v libx264 -crf 20 -pix_fmt yuv420p grafo.mp4
La salida del grafo. La señal se bifurcó en dos, una rama pasó por detección de bordes, y las dos se volvieron a unir por diferencia. Tres nodos, cuatro aristas.

Que alguien nombre en voz alta lo que está viendo, porque la respuesta es un concepto que ya conoce con otro nombre:

  • split es una bifurcación (fan-out)
  • blend es una unión (join)
  • Las etiquetas [base], [fx], [ghost] son aristas
  • El conjunto es un grafo dirigido acíclico, ordenado topológicamente y evaluado fotograma por fotograma

Es la misma forma que un sistema de build, un job de Spark, un árbol de render de React o un pipeline de CI. Distinto dominio, misma estructura de datos.

Después, la comparación que cierra:

# Un proceso, un grafo interno
ffmpeg -i clip.mp4 -vf "hue=s=0,edgedetect" -f null -

# Dos procesos, un pipe del sistema operativo
ffmpeg -i clip.mp4 -vf "hue=s=0" -f nut - | ffmpeg -i - -vf edgedetect -f null -

El momento Semántica idéntica, planificador distinto, y consumo de memoria medible y distinto. La diferencia entre esas dos líneas es la diferencia entre un runtime que administra el flujo de datos y el sistema operativo haciéndolo por vos. Es la misma pregunta que resuelven Airflow, make y las tuberías de Unix.

Módulo 6

Exportar y proyectar

20 min

Concepto Cierre y evaluación

Cada persona exporta 10 segundos y los proyectamos todos seguidos. Antes de que se reproduzca el suyo, dice una sola frase: qué suposición viola su clip.

Esa frase es toda la evaluación del taller. Si puede decirla, entendió una máquina de estados, propagación de errores y compresión con pérdida. Si no puede, el clip queda igual de lindo — pero volvemos 20 minutos al módulo 3.

ffmpeg -y -i final.mp4 -t 10 -c:v libx264 -crf 20 -pix_fmt yuv420p \
       -vf "scale=1280:-2" entrega.mp4

El momento El que aparece cuando alguien mira el clip de otro y dice, sin que se lo pidan, "ah, a ese le sacaste los keyframes del segundo clip". Ahí dejaron de ver un efecto y empezaron a ver un mecanismo.

05 Para seguir

Ordenado por dificultad. El primero es el que más enseña.

  1. Moshear a mano con un editor hexadecimal

    Abrí el AVI con xxd, encontrá los chunks 00dc dentro de la lista movi, ubicá el índice idx1 y dale vuelta el bit AVIIF_KEYFRAME (0x10). Cuando termines habrás escrito un parser de formato binario a mano, que es el verdadero premio de todo el taller.

    xxd moshed.avi | head -40
    xxd moshed.avi | grep -m5 '00dc'
  2. Duplicación de fotogramas P

    El efecto clásico de "florecimiento": repetir el chunk de un solo fotograma P treinta veces, para que un mismo campo de movimiento se aplique una y otra vez sobre la imagen. Son unas 40 líneas de Python sobre chunks RIFF. Es el efecto del video de Evident Utensil de Chairlift, que en 2009 puso el datamoshing en MTV.

  3. Fotogramas a Python y vuelta

    Pipe de video crudo a numpy, mutación en Python, pipe de vuelta a ffmpeg. Glitch art como ejercicio de procesamiento de streams.

    ffmpeg -i clip.mp4 -f rawvideo -pix_fmt rgb24 - \
      | python3 mutar.py \
      | ffmpeg -f rawvideo -pix_fmt rgb24 -s 640x360 -r 25 -i - salida.mp4
  4. Moshear H.264 y explicar por qué cuesta tanto más

    Intentá el mismo procedimiento sobre H.264 y documentá por qué se resiste: slices, múltiples fotogramas de referencia, filtro de desbloqueo en el lazo, CABAC. Conclusión: cada capa que se agregó para ganar eficiencia agregó también resistencia a errores. La fragilidad de MPEG-4 no es un defecto de MPEG-4, es la ausencia de veinte años de trabajo.

06 Glosario

El vocabulario de video está casi todo en inglés. Estas son las traducciones que usamos, con el término original al lado para que puedan googlear.

Contenedor container
El envase: mp4, mkv, avi. Dice qué pistas hay y dónde. No comprime nada.
Códec codec
El algoritmo que comprime y descomprime: h264, mpeg4, av1. Coder-decoder.
Bitstream
La secuencia concreta de bytes que consume el decodificador.
Remuxear remux
Cambiar de contenedor sin recodificar. Sin pérdida.
Fotograma clave keyframe, I-frame
Imagen completa, decodificable por sí sola. Punto de entrada.
Fotograma P P-frame
Predicho a partir del anterior. Guarda movimiento y corrección, no imagen.
Fotograma B B-frame
Predicho a partir del pasado y del futuro simultáneamente.
GOP
Group of Pictures. Un fotograma clave y todos los predichos que dependen de él.
Vector de movimiento motion vector
"Este bloque se movió tanto para allá". La unidad de información de un fotograma P.
Submuestreo de croma chroma subsampling
Guardar menos resolución de color que de brillo. El 4:2:0 de yuv420p.
Cuantización quantization
Redondear para ocupar menos. Es donde se pierde la información, literalmente.
PSNR / SSIM
Métricas objetivas de diferencia entre dos videos. Más dB es más parecido.
Filtro de bitstream bitstream filter
Modifica el flujo comprimido sin decodificarlo. Nuestro noise es uno.
Ocultación de errores error concealment
Rellenar información faltante para que el video degrade en vez de cortarse. El motor del datamosh.
DAG
Grafo dirigido acíclico. La forma de un grafo de filtros, y de casi todo pipeline de datos.